Repository: micrometer-metrics/micrometer
Issue: Fix logic to choose ObservationConvention

There are two issues with the logic that chooses and applies the `ObservationConvention` on the `Observation`s.

1. The logic can choose and apply multiple conventions, also in one case it can mix them together
2. In one case the logic pics the wrong convention

Here's the old and the new decision matrix:

**OLD:**
```
1. custom     default     no pre-cofigured  -> custom
2. custom     default     pre-cofigured     -> custom on top of pre-cofigured
3. no custom  default     no pre-cofigured  -> default
4. no custom  default     pre-cofigured     -> default on top of pre-cofigured (*)
5. custom     no default  no pre-cofigured  -> custom
6. custom     no default  pre-cofigured     -> custom on top of pre-cofigured
7. no custom  no default  no pre-cofigured  -> local
8. no custom  no default  pre-cofigured     -> pre-cofigured
```
_(*): This logic is wrong: 1. they are mixed, 2. pre-cofigured should have choosen_

**NEW:**
```
1. custom     default     no pre-configured  -> custom
2. custom     default     pre-configured     -> custom (not really a valid case, use custom)
3. no custom  default     no pre-configured  -> default
4. no custom  default     pre-configured     -> pre-configured
5. custom     no default  no pre-configured  -> custom (providing default is recommended)
6. custom     no default  pre-configured     -> custom (providing default is recommended)
7. no custom  no default  no pre-configured  -> local names/tags will be used
8. no custom  no default  pre-configured     -> pre-configured
```

cc: @bclozel 

Changed files:
micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java
micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java
micrometer-observation/src/main/java/io/micrometer/observation/SimpleObservation.java
micrometer-observation/src/main/java/io/micrometer/observation/Observation.java
micrometer-observation/src/test/java/io/micrometer/observation/ObservationRegistryTest.java
micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryCompatibilityKit.java

diff --git a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryCompatibilityKit.java b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryCompatibilityKit.java
index 0d4ef3a38..ffb7cf982 100644
--- a/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryCompatibilityKit.java
+++ b/micrometer-observation-test/src/main/java/io/micrometer/observation/tck/ObservationRegistryCompatibilityKit.java
@@ -765,8 +765,6 @@ public abstract class ObservationRegistryCompatibilityKit {
                 .highCardinalityKeyValue("hcTag1", "0")
                 // should override the previous line
                 .highCardinalityKeyValue("hcTag1", "3").highCardinalityKeyValues(KeyValues.of("hcTag2", "4"))
-                .observationConvention(new TestObservationConvention("local"))
-                .observationConvention(new UnsupportedObservationConvention("local"))
                 .contextualName("test.observation.42").error(exception).start();
         observation.stop();
 
@@ -774,16 +772,13 @@ public abstract class ObservationRegistryCompatibilityKit {
             assertThat(context).isSameAs(testContext);
             assertThat(context.getName()).isEqualTo("test.observation");
             assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("lcTag1", "1"),
-                    KeyValue.of("lcTag2", "2"), KeyValue.of("local.context.class", "TestContext"),
-                    KeyValue.of("global.context.class", "TestContext"));
+                    KeyValue.of("lcTag2", "2"), KeyValue.of("global.context.class", "TestContext"));
             assertThat(context.getHighCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("hcTag1", "3"),
-                    KeyValue.of("hcTag2", "4"), KeyValue.of("local.uuid", testContext.uuid),
-                    KeyValue.of("global.uuid", testContext.uuid));
+                    KeyValue.of("hcTag2", "4"), KeyValue.of("global.uuid", testContext.uuid));
 
             assertThat(context.getAllKeyValues()).containsExactlyInAnyOrder(KeyValue.of("lcTag1", "1"),
-                    KeyValue.of("lcTag2", "2"), KeyValue.of("local.context.class", "TestContext"),
-                    KeyValue.of("global.context.class", "TestContext"), KeyValue.of("hcTag1", "3"),
-                    KeyValue.of("hcTag2", "4"), KeyValue.of("local.uuid", testContext.uuid),
+                    KeyValue.of("lcTag2", "2"), KeyValue.of("global.context.class", "TestContext"),
+                    KeyValue.of("hcTag1", "3"), KeyValue.of("hcTag2", "4"),
                     KeyValue.of("global.uuid", testContext.uuid));
 
             assertThat((String) context.get("context.field")).isEqualTo("42");
@@ -795,9 +790,9 @@ public abstract class ObservationRegistryCompatibilityKit {
                     .containsOnlyOnce("contextualName='test.observation.42'")
                     .containsOnlyOnce("error='java.io.IOException: simulated'")
                     .containsOnlyOnce(
-                            "lowCardinalityKeyValues=[global.context.class='TestContext', lcTag1='1', lcTag2='2', local.context.class='TestContext']")
-                    .containsOnlyOnce("highCardinalityKeyValues=[global.uuid='" + testContext.uuid
-                            + "', hcTag1='3', hcTag2='4', local.uuid='" + testContext.uuid + "']")
+                            "lowCardinalityKeyValues=[global.context.class='TestContext', lcTag1='1', lcTag2='2']")
+                    .containsOnlyOnce(
+                            "highCardinalityKeyValues=[global.uuid='" + testContext.uuid + "', hcTag1='3', hcTag2='4']")
                     .containsOnlyOnce("map=[context.field='42']");
         });
     }
diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java b/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java
index 7ae94e224..17fde2019 100644
--- a/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java
+++ b/micrometer-observation/src/main/java/io/micrometer/observation/Observation.java
@@ -127,6 +127,7 @@ public interface Observation extends ObservationView {
         return new SimpleObservation(name, registry, context == null ? new Context() : context);
     }
 
+    // @formatter:off
     /**
      * Creates but <b>does not start</b> an {@link Observation}. Remember to call
      * {@link Observation#start()} when you want the measurements to start. When the
@@ -143,15 +144,37 @@ public interface Observation extends ObservationView {
      * {@link ObservationRegistry.ObservationConfig#getObservationConvention(Context, ObservationConvention)})
      * was found. The {@link ObservationConvention} implementation can override
      * {@link Observation} names (i.e. name and contextual name) and key values.
+     * <pre>
+     * Here you can find the matrix of choosing the convention:
+     * 1. custom     default     no pre-configured  -> custom
+     * 2. custom     default     pre-configured     -> custom (not a valid case, just use custom)
+     * 3. no custom  default     no pre-configured  -> default
+     * 4. no custom  default     pre-configured     -> pre-configured
+     * 5. custom     no default  no pre-configured  -> custom (providing default is recommended)
+     * 6. custom     no default  pre-configured     -> custom (providing default is recommended)
+     * 7. no custom  no default  no pre-configured  -> local names/tags will be used
+     * 8. no custom  no default  pre-configured     -> pre-configured
+     * </pre>
+     * <p>
+     * Also, if you set a convention using,
+     * {@link Observation#observationConvention(ObservationConvention)} (not recommended),
+     * the convention you set here will be used and everything else (custom, default,
+     * pre-configured) will be ignored.
+     * </p>
+     * <p>
+     * If you want to add to the all the contexts or mutate them,
+     * use the ObservationFilter (e.g.: add region=cloud-1 or remove PII).
+     * </p>
      * @param <T> type of context
      * @param customConvention custom convention. If {@code null}, the default one will be
      * picked.
      * @param defaultConvention default convention when no custom convention was passed,
-     * nor a configured one was found
+     * nor a pre-configured one was found
      * @param contextSupplier supplier for the observation context
      * @param registry observation registry
      * @return created but not started observation
      */
+    // @formatter:on
     static <T extends Context> Observation createNotStarted(@Nullable ObservationConvention<T> customConvention,
             ObservationConvention<T> defaultConvention, Supplier<T> contextSupplier, ObservationRegistry registry) {
         if (registry.isNoop()) {
@@ -174,6 +197,11 @@ public interface Observation extends ObservationView {
     /**
      * Creates and starts an {@link Observation}. When no registry is passed or
      * observation is not applicable will return a no-op observation.
+     * <p>
+     * Please check the javadoc of
+     * {@link Observation#createNotStarted(ObservationConvention, ObservationConvention, Supplier, ObservationRegistry)}
+     * method for the logic of choosing the convention.
+     * </p>
      * @param observationConvention observation convention
      * @param registry observation registry
      * @return started observation
@@ -192,6 +220,11 @@ public interface Observation extends ObservationView {
      * {@link ObservationRegistry.ObservationConfig#observationPredicate(ObservationPredicate)
      * ObservationConfig#observationPredicate}), a no-op observation will also be
      * returned.
+     * <p>
+     * Please check the javadoc of
+     * {@link Observation#createNotStarted(ObservationConvention, ObservationConvention, Supplier, ObservationRegistry)}
+     * method for the logic of choosing the convention.
+     * </p>
      * @param <T> type of context
      * @param observationConvention observation convention
      * @param contextSupplier mutable context supplier
@@ -216,6 +249,11 @@ public interface Observation extends ObservationView {
      * provide a default one if neither a custom nor a pre-configured one (via
      * {@link ObservationRegistry.ObservationConfig#getObservationConvention(Context, ObservationConvention)})
      * was found.
+     * <p>
+     * Please check the javadoc of
+     * {@link Observation#createNotStarted(ObservationConvention, ObservationConvention, Supplier, ObservationRegistry)}
+     * method for the logic of choosing the convention.
+     * </p>
      * @param <T> type of context
      * @param registry observation registry
      * @param contextSupplier the observation context supplier
@@ -235,6 +273,11 @@ public interface Observation extends ObservationView {
      * {@link Observation#start()} when you want the measurements to start. When no
      * registry is passed or observation is not applicable will return a no-op
      * observation.
+     * <p>
+     * Please check the javadoc of
+     * {@link Observation#createNotStarted(ObservationConvention, ObservationConvention, Supplier, ObservationRegistry)}
+     * method for the logic of choosing the convention.
+     * </p>
      * @param observationConvention observation convention
      * @param registry observation registry
      * @return created but not started observation
@@ -265,6 +308,11 @@ public interface Observation extends ObservationView {
      * of {@link Context} to be passed and if you're not providing one we won't be able to
      * initialize it ourselves.
      * </p>
+     * <p>
+     * Please check the javadoc of
+     * {@link Observation#createNotStarted(ObservationConvention, ObservationConvention, Supplier, ObservationRegistry)}
+     * method for the logic of choosing the convention.
+     * </p>
      * @param <T> type of context
      * @param observationConvention observation convention
      * @param contextSupplier mutable context supplier
@@ -380,8 +428,8 @@ public interface Observation extends ObservationView {
 
     /**
      * Adds an observation convention that can be used to attach key values to the
-     * observation. WARNING: You must add ObservationConvention instances to the
-     * Observation before it is started.
+     * observation. WARNING: You must set the ObservationConvention to the Observation
+     * before it is started.
      * @param observationConvention observation convention
      * @return this
      */
diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java b/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java
index 93e51050a..7381eaec1 100644
--- a/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java
+++ b/micrometer-observation/src/main/java/io/micrometer/observation/ObservationRegistry.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Supplier;
 
 /**
  * Implementations of this interface are responsible for managing state of an
@@ -135,6 +136,11 @@ public interface ObservationRegistry {
 
         /**
          * Register an {@link ObservationConvention}.
+         * <p>
+         * Please check the javadoc of
+         * {@link Observation#createNotStarted(ObservationConvention, ObservationConvention, Supplier, ObservationRegistry)}
+         * method for the logic of choosing the convention.
+         * </p>
          * @param observationConvention observation convention
          * @return This configuration instance
          */
diff --git a/micrometer-observation/src/main/java/io/micrometer/observation/SimpleObservation.java b/micrometer-observation/src/main/java/io/micrometer/observation/SimpleObservation.java
index b70cab91a..7aaaa2a39 100644
--- a/micrometer-observation/src/main/java/io/micrometer/observation/SimpleObservation.java
+++ b/micrometer-observation/src/main/java/io/micrometer/observation/SimpleObservation.java
@@ -38,8 +38,9 @@ class SimpleObservation implements Observation {
 
     private final Context context;
 
+    @Nullable
     @SuppressWarnings("rawtypes")
-    private final Collection<ObservationConvention> conventions;
+    private ObservationConvention convention;
 
     @SuppressWarnings("rawtypes")
     private final Deque<ObservationHandler> handlers;
@@ -50,16 +51,20 @@ class SimpleObservation implements Observation {
         this.registry = registry;
         this.context = context;
         this.context.setName(name);
-        this.conventions = getConventionsFromConfig(registry, context);
+        this.convention = getConventionFromConfig(registry, context);
         this.handlers = getHandlersFromConfig(registry, context);
         this.filters = registry.observationConfig().getObservationFilters();
     }
 
     SimpleObservation(ObservationConvention<? extends Context> convention, ObservationRegistry registry,
             Context context) {
-        this((String) null, registry, context); // name is set later in start()
+        this.registry = registry;
+        this.context = context;
+        // name is set later in start()
+        this.handlers = getHandlersFromConfig(registry, context);
+        this.filters = registry.observationConfig().getObservationFilters();
         if (convention.supportsContext(context)) {
-            this.conventions.add(convention);
+            this.convention = convention;
         }
         else {
             throw new IllegalStateException(
@@ -67,10 +72,10 @@ class SimpleObservation implements Observation {
         }
     }
 
-    private static Collection<ObservationConvention> getConventionsFromConfig(ObservationRegistry registry,
-            Context context) {
+    @Nullable
+    private static ObservationConvention getConventionFromConfig(ObservationRegistry registry, Context context) {
         return registry.observationConfig().getObservationConventions().stream()
-                .filter(convention -> convention.supportsContext(context)).collect(Collectors.toList());
+                .filter(convention -> convention.supportsContext(context)).findFirst().orElse(null);
     }
 
     private static Deque<ObservationHandler> getHandlersFromConfig(ObservationRegistry registry, Context context) {
@@ -105,7 +110,7 @@ class SimpleObservation implements Observation {
     @Override
     public Observation observationConvention(ObservationConvention<?> convention) {
         if (convention.supportsContext(context)) {
-            this.conventions.add(convention);
+            this.convention = convention;
         }
         return this;
     }
@@ -125,18 +130,13 @@ class SimpleObservation implements Observation {
 
     @Override
     public Observation start() {
-        // We want to rename with the first matching convention
-        boolean nameChanged = false;
-        for (ObservationConvention convention : this.conventions) {
+        if (this.convention != null) {
             this.context.addLowCardinalityKeyValues(convention.getLowCardinalityKeyValues(context));
             this.context.addHighCardinalityKeyValues(convention.getHighCardinalityKeyValues(context));
 
-            if (!nameChanged) {
-                String newName = convention.getName();
-                if (StringUtils.isNotBlank(newName)) {
-                    this.context.setName(newName);
-                    nameChanged = true;
-                }
+            String newName = convention.getName();
+            if (StringUtils.isNotBlank(newName)) {
+                this.context.setName(newName);
             }
         }
 
@@ -152,18 +152,13 @@ class SimpleObservation implements Observation {
     @SuppressWarnings({ "rawtypes", "unchecked" })
     @Override
     public void stop() {
-        // We want to rename with the first matching convention
-        boolean contextualNameChanged = false;
-        for (ObservationConvention convention : this.conventions) {
+        if (this.convention != null) {
             this.context.addLowCardinalityKeyValues(convention.getLowCardinalityKeyValues(context));
             this.context.addHighCardinalityKeyValues(convention.getHighCardinalityKeyValues(context));
 
-            if (!contextualNameChanged) {
-                String newContextualName = convention.getContextualName(context);
-                if (StringUtils.isNotBlank(newContextualName)) {
-                    this.context.setContextualName(newContextualName);
-                    contextualNameChanged = true;
-                }
+            String newContextualName = convention.getContextualName(context);
+            if (StringUtils.isNotBlank(newContextualName)) {
+                this.context.setContextualName(newContextualName);
             }
         }
 
diff --git a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationRegistryTest.java b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationRegistryTest.java
index d2d36fb5c..2832fd921 100644
--- a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationRegistryTest.java
+++ b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationRegistryTest.java
@@ -96,8 +96,6 @@ class ObservationRegistryTest {
         registry.observationConfig().observationHandler(c -> true);
         // Define a convention
         MessagingConvention messagingConvention = new OurCompanyStandardMessagingConvention();
-        // Register a semantic name provider
-        registry.observationConfig().observationConvention(new OurCompanyObservationConvention());
 
         Observation.Context myContext = new MessagingContext().put("foo", "hello");
         // Observation convention wants to use a MessagingConvention
@@ -125,6 +123,12 @@ class ObservationRegistryTest {
             this.messagingConvention = messagingConvention;
         }
 
+        // Here we override the default "observation" name
+        @Override
+        public String getName() {
+            return "new name";
+        }
+
         @Override
         public KeyValues getLowCardinalityKeyValues(MessagingContext context) {
             return KeyValues.of(this.messagingConvention.queueName(context.get("foo")));
@@ -153,22 +157,4 @@ class ObservationRegistryTest {
 
     }
 
-    static class OurCompanyObservationConvention implements GlobalObservationConvention<Observation.Context> {
-
-        // Here we override the default "observation" name
-        @Override
-        public String getName() {
-            return "new name";
-        }
-
-        // This semantic name provider is only applicable when we're using a messaging
-        // context
-
-        @Override
-        public boolean supportsContext(Observation.Context context) {
-            return context instanceof MessagingContext;
-        }
-
-    }
-
 }
diff --git a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java
index 39cc1f19f..81d3d2a55 100644
--- a/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java
+++ b/micrometer-observation/src/test/java/io/micrometer/observation/ObservationTests.java
@@ -16,6 +16,8 @@
 package io.micrometer.observation;
 
 import io.micrometer.common.KeyValue;
+import io.micrometer.common.KeyValues;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
@@ -32,7 +34,12 @@ import static org.assertj.core.api.Assertions.assertThat;
  */
 class ObservationTests {
 
-    private final ObservationRegistry registry = ObservationRegistry.create();
+    private ObservationRegistry registry;
+
+    @BeforeEach
+    void setUp() {
+        this.registry = ObservationRegistry.create();
+    }
 
     @Test
     void notHavingAnyHandlersShouldResultInNoopObservation() {
@@ -150,6 +157,138 @@ class ObservationTests {
         }
     }
 
+    @Test
+    void customAndDefaultAndNoGlobalConventionShouldResolveToCustomConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(new CustomConvention(), new DefaultConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("custom.name");
+        assertThat(context.getContextualName()).isEqualTo("custom.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("custom", "present"), KeyValue.of("low", "custom"));
+    }
+
+    @Test
+    void customAndDefaultAndGlobalConventionShouldResolveToCustomConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+        registry.observationConfig().observationConvention(new GlobalConvention());
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(new CustomConvention(), new DefaultConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("custom.name");
+        assertThat(context.getContextualName()).isEqualTo("custom.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("custom", "present"), KeyValue.of("low", "custom"));
+    }
+
+    @Test
+    void noCustomAndDefaultAndNoGlobalConventionShouldResolveToDefaultConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(null, new DefaultConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("default.name");
+        assertThat(context.getContextualName()).isEqualTo("default.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("default", "present"), KeyValue.of("low", "default"));
+    }
+
+    @Test
+    void noCustomAndDefaultAndGlobalConventionShouldResolveToDefaultConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+        registry.observationConfig().observationConvention(new GlobalConvention());
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(null, new DefaultConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("global.name");
+        assertThat(context.getContextualName()).isEqualTo("global.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("global", "present"), KeyValue.of("low", "global"));
+    }
+
+    @Test
+    void customAndNoDefaultAndNoGlobalConventionShouldResolveToCustomConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(new CustomConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("custom.name");
+        assertThat(context.getContextualName()).isEqualTo("custom.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("custom", "present"), KeyValue.of("low", "custom"));
+    }
+
+    @Test
+    void customAndNoDefaultAndGlobalConventionShouldResolveToCustomConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+        registry.observationConfig().observationConvention(new GlobalConvention());
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(new CustomConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("custom.name");
+        assertThat(context.getContextualName()).isEqualTo("custom.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("custom", "present"), KeyValue.of("low", "custom"));
+    }
+
+    @Test
+    void noCustomAndNoDefaultAndNoGlobalConventionShouldResolveToNoConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted("local.name", () -> context, registry).contextualName("local.contextualName")
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("local.name");
+        assertThat(context.getContextualName()).isEqualTo("local.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("low", "local"));
+    }
+
+    @Test
+    void noCustomAndNoDefaultAndGlobalConventionShouldResolveToGlobalConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+        registry.observationConfig().observationConvention(new GlobalConvention());
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted("local.name", () -> context, registry).contextualName("local.contextualName")
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local").start().stop();
+
+        assertThat(context.getName()).isEqualTo("global.name");
+        assertThat(context.getContextualName()).isEqualTo("global.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("global", "present"), KeyValue.of("low", "global"));
+    }
+
+    @Test
+    void instrumentationAndCustomAndDefaultAndGlobalConventionShouldResolveToInstrumentationConvention() {
+        registry.observationConfig().observationHandler(context -> true);
+        registry.observationConfig().observationConvention(new GlobalConvention());
+
+        Observation.Context context = new Observation.Context();
+        Observation.createNotStarted(new CustomConvention(), new DefaultConvention(), () -> context, registry)
+                .lowCardinalityKeyValue("local", "present").lowCardinalityKeyValue("low", "local")
+                .observationConvention(new InstrumentationConvention()).start().stop();
+
+        assertThat(context.getName()).isEqualTo("instrumentation.name");
+        assertThat(context.getContextualName()).isEqualTo("instrumentation.contextualName");
+        assertThat(context.getLowCardinalityKeyValues()).containsExactlyInAnyOrder(KeyValue.of("local", "present"),
+                KeyValue.of("instrumentation", "present"), KeyValue.of("low", "instrumentation"));
+    }
+
     static class Service {
 
         String executeCallable() throws IOException {
@@ -167,4 +306,100 @@ class ObservationTests {
 
     }
 
+    static class CustomConvention implements ObservationConvention<Observation.Context> {
+
+        @Override
+        public String getName() {
+            return "custom.name";
+        }
+
+        @Override
+        public String getContextualName(Observation.Context context) {
+            return "custom.contextualName";
+        }
+
+        @Override
+        public KeyValues getLowCardinalityKeyValues(Observation.Context context) {
+            return KeyValues.of("low", "custom", "custom", "present");
+        }
+
+        @Override
+        public boolean supportsContext(Observation.Context context) {
+            return true;
+        }
+
+    }
+
+    static class DefaultConvention implements ObservationConvention<Observation.Context> {
+
+        @Override
+        public String getName() {
+            return "default.name";
+        }
+
+        @Override
+        public String getContextualName(Observation.Context context) {
+            return "default.contextualName";
+        }
+
+        @Override
+        public KeyValues getLowCardinalityKeyValues(Observation.Context context) {
+            return KeyValues.of("low", "default", "default", "present");
+        }
+
+        @Override
+        public boolean supportsContext(Observation.Context context) {
+            return true;
+        }
+
+    }
+
+    static class GlobalConvention implements GlobalObservationConvention<Observation.Context> {
+
+        @Override
+        public String getName() {
+            return "global.name";
+        }
+
+        @Override
+        public String getContextualName(Observation.Context context) {
+            return "global.contextualName";
+        }
+
+        @Override
+        public KeyValues getLowCardinalityKeyValues(Observation.Context context) {
+            return KeyValues.of("low", "global", "global", "present");
+        }
+
+        @Override
+        public boolean supportsContext(Observation.Context context) {
+            return true;
+        }
+
+    }
+
+    static class InstrumentationConvention implements ObservationConvention<Observation.Context> {
+
+        @Override
+        public String getName() {
+            return "instrumentation.name";
+        }
+
+        @Override
+        public String getContextualName(Observation.Context context) {
+            return "instrumentation.contextualName";
+        }
+
+        @Override
+        public KeyValues getLowCardinalityKeyValues(Observation.Context context) {
+            return KeyValues.of("low", "instrumentation", "instrumentation", "present");
+        }
+
+        @Override
+        public boolean supportsContext(Observation.Context context) {
+            return true;
+        }
+
+    }
+
 }