Repository: open-telemetry/opentelemetry-python

Issue:
v1.23.0 added an abstract method to Meter, breaking subclasses 
We're developing an SDK that uses OpenTelemetry. This morning a user reported that after installing the SDK, everything they tried led to this error:

```
TypeError: Can't instantiate abstract class _ProxyMeter with abstract method create_gauge
```

`_ProxyMeter` here is our own subclass of `Meter`.

The changelog says:

> this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

I think releasing this change violates semver, as it was a minor version release that created errors in previously compliant code.

If this is intentional because users aren't supposed to subclass `Meter` themselves, please let me know so that we can constrain the dependency to `<1.24` to avoid any future surprises. We have subclasses of `Span`, `Tracer`, `TracerProvider`, `MeterProvider`, `Meter`, and probably others, so from our perspective the abstract base classes need to be stable.

If this wasn't intentional, then in future I think you could either:

1. Release such changes in a major version, or
2. Add new 'abstract' methods to abstract base classes as normal methods (i.e. not decorated with `@abstractmethod`) but that simply `raise NotImplementedError`. That way subclasses can still be instantiated.

We've resolved the problem in our own library, but it's likely that there are other libraries currently giving new users the same error. That could be mitigated here by either:

1. Releasing 2.0.0 with the same code, then yanking 1.23.0 from PyPI. This means that `pip install opentelemetry-sdk<2` will install 1.22.0, while `pip install opentelemetry-sdk==1.23.0` will still work.
2. Releasing 1.23.1 with a backwards compatible fix such as (2) above.


Discussion:
this issue is happening with us as well from yesterday, got below stacktrace while initialising opentelemetry metrics. we have also developed our own sdk on top of otel.

```File "/.venv/lib/python3.8/site-packages/opentelemetry/metrics/_internal/__init__.py", line 811, in set_meter_provider
otel_srvr |     _set_meter_provider(meter_provider, log=True)
otel_srvr |   File "/.venv/lib/python3.8/site-packages/opentelemetry/metrics/_internal/__init__.py", line 799, in _set_meter_provider
otel_srvr |     did_set = _METER_PROVIDER_SET_ONCE.do_once(set_mp)
otel_srvr |   File "/.venv/lib/python3.8/site-packages/opentelemetry/util/_once.py", line 44, in do_once
otel_srvr |     func()
otel_srvr |   File "/.venv/lib/python3.8/site-packages/opentelemetry/metrics/_internal/__init__.py", line 797, in set_mp
otel_srvr |     _PROXY_METER_PROVIDER.on_set_meter_provider(meter_provider)
otel_srvr |   File "/.venv/lib/python3.8/site-packages/opentelemetry/metrics/_internal/__init__.py", line 173, in on_set_meter_provider
otel_srvr |     meter.on_set_meter_provider(meter_provider)
otel_srvr |   File "/.venv/lib/python3.8/site-packages/opentelemetry/metrics/_internal/__init__.py", line 463, in on_set_meter_provider
otel_srvr |     real_meter = meter_provider.get_meter(
otel_srvr |   File "/.venv/lib/python3.8/site-packages/opentelemetry/sdk/metrics/_internal/__init__.py", line 488, in get_meter
otel_srvr |     self._meters[info] = Meter(
otel_srvr | TypeError: Can't instantiate abstract class Meter with abstract methods create_gauge```
Are you sure this isn't a 3rd party interaction with the new code?

Wrote a small program that uses OTel to push network performance to a dashboard. The previous version used OpenCensus.  Last week I installed it on a Mac and on a Windows machine with zero issues.  Today I installed it on a Raspberry Pi and it broke with the same last line as your logs.
This is almost certainly unrelated but...

Raspberry Pi which had this symptom was having trouble with dependency resolution.

I was on pip 20.1.  Upgraded to pip 24.0.  Re-fetched my project dependencies. with `pip3 install -r requirements.txt` Now my program runs fine. 
This wasn't intentional (we added the method `add_link` which wasn't abstract although all the existing methods are abstract and simply warn no-op, to honour the semver guarantees https://github.com/open-telemetry/opentelemetry-python/pull/3618). However, this was overlooked during the `create_gauge` feature.  We should have some tests/tooling to catch this kind of issue.  
This was definitely a mistake

> 2\. Releasing 1.23.1 with a backwards compatible fix such as (2) above.

Can someone send a PR and we can try to make a patch release ASAP?

Resolving patch:
diff --git a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py
--- a/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py
+++ b/opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py
@@ -41,6 +41,7 @@
 """
 
 
+import warnings
 from abc import ABC, abstractmethod
 from logging import getLogger
 from os import environ
@@ -385,8 +386,7 @@ def create_histogram(
             description: A description for this instrument and what it measures.
         """
 
-    @abstractmethod
-    def create_gauge(
+    def create_gauge(  # type: ignore # pylint: disable=no-self-use
         self,
         name: str,
         unit: str = "",
@@ -400,6 +400,7 @@ def create_gauge(
                 example, ``By`` for bytes. UCUM units are recommended.
             description: A description for this instrument and what it measures.
         """
+        warnings.warn("create_gauge() is not implemented and will be a no-op")
 
     @abstractmethod
     def create_observable_gauge(


Test patch:
diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py
--- a/opentelemetry-api/tests/metrics/test_meter.py
+++ b/opentelemetry-api/tests/metrics/test_meter.py
@@ -134,7 +134,6 @@ def test_create_gauge(self):
         """
 
         self.assertTrue(hasattr(Meter, "create_gauge"))
-        self.assertTrue(Meter.create_gauge.__isabstractmethod__)
 
     def test_create_observable_gauge(self):
         """
diff --git a/opentelemetry-api/tests/metrics/test_subclass_instantiation.py b/opentelemetry-api/tests/metrics/test_subclass_instantiation.py
new file mode 100644
--- /dev/null
+++ b/opentelemetry-api/tests/metrics/test_subclass_instantiation.py
@@ -0,0 +1,209 @@
+# Copyright The OpenTelemetry Authors
+#
+# 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.
+# type: ignore
+
+# NOTE: The tests in this file are intended to test the semver compatibility of the public API.
+# Any tests that fail here indicate that the public API has changed in a way that is not backwards compatible.
+# Either bump the major version of the API, or make the necessary changes to the API to remain semver compatible.
+
+from typing import Optional
+
+from opentelemetry.metrics import (
+    Asynchronous,
+    Counter,
+    Histogram,
+    Instrument,
+    Meter,
+    MeterProvider,
+    ObservableCounter,
+    ObservableGauge,
+    ObservableUpDownCounter,
+    Synchronous,
+    UpDownCounter,
+    _Gauge,
+)
+
+
+class MeterProviderImplTest(MeterProvider):
+    def get_meter(
+        self,
+        name: str,
+        version: Optional[str] = None,
+        schema_url: Optional[str] = None,
+    ) -> Meter:
+        return super().get_meter(name, version, schema_url)
+
+
+def test_meter_provider_subclass_instantiation():
+    meter_provider = MeterProviderImplTest()
+    assert isinstance(meter_provider, MeterProvider)
+
+
+class MeterImplTest(Meter):
+    def create_counter(self, name, description, **kwargs):
+        pass
+
+    def create_up_down_counter(self, name, description, **kwargs):
+        pass
+
+    def create_observable_counter(self, name, description, **kwargs):
+        pass
+
+    def create_histogram(self, name, description, **kwargs):
+        pass
+
+    def create_observable_gauge(self, name, description, **kwargs):
+        pass
+
+    def create_observable_up_down_counter(self, name, description, **kwargs):
+        pass
+
+
+def test_meter_subclass_instantiation():
+    meter = MeterImplTest("subclass_test")
+    assert isinstance(meter, Meter)
+
+
+class SynchronousImplTest(Synchronous):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+
+def test_synchronous_subclass_instantiation():
+    synchronous = SynchronousImplTest("subclass_test")
+    assert isinstance(synchronous, Synchronous)
+
+
+class AsynchronousImplTest(Asynchronous):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+
+def test_asynchronous_subclass_instantiation():
+    asynchronous = AsynchronousImplTest("subclass_test")
+    assert isinstance(asynchronous, Asynchronous)
+
+
+class CounterImplTest(Counter):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+    def add(self, amount: int, **kwargs):
+        pass
+
+
+def test_counter_subclass_instantiation():
+    counter = CounterImplTest("subclass_test")
+    assert isinstance(counter, Counter)
+
+
+class UpDownCounterImplTest(UpDownCounter):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+    def add(self, amount: int, **kwargs):
+        pass
+
+
+def test_up_down_counter_subclass_instantiation():
+    up_down_counter = UpDownCounterImplTest("subclass_test")
+    assert isinstance(up_down_counter, UpDownCounter)
+
+
+class ObservableCounterImplTest(ObservableCounter):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+
+def test_observable_counter_subclass_instantiation():
+    observable_counter = ObservableCounterImplTest("subclass_test")
+    assert isinstance(observable_counter, ObservableCounter)
+
+
+class HistogramImplTest(Histogram):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+    def record(self, amount: int, **kwargs):
+        pass
+
+
+def test_histogram_subclass_instantiation():
+    histogram = HistogramImplTest("subclass_test")
+    assert isinstance(histogram, Histogram)
+
+
+class GaugeImplTest(_Gauge):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+    def set(self, amount: int, **kwargs):
+        pass
+
+
+def test_gauge_subclass_instantiation():
+    gauge = GaugeImplTest("subclass_test")
+    assert isinstance(gauge, _Gauge)
+
+
+class InstrumentImplTest(Instrument):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+
+def test_instrument_subclass_instantiation():
+    instrument = InstrumentImplTest("subclass_test")
+    assert isinstance(instrument, Instrument)
+
+
+class ObservableGaugeImplTest(ObservableGauge):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+
+def test_observable_gauge_subclass_instantiation():
+    observable_gauge = ObservableGaugeImplTest("subclass_test")
+    assert isinstance(observable_gauge, ObservableGauge)
+
+
+class ObservableUpDownCounterImplTest(ObservableUpDownCounter):
+    def __init__(
+        self, name: str, unit: str = "", description: str = ""
+    ) -> None:
+        super().__init__(name, unit, description)
+
+
+def test_observable_up_down_counter_subclass_instantiation():
+    observable_up_down_counter = ObservableUpDownCounterImplTest(
+        "subclass_test"
+    )
+    assert isinstance(observable_up_down_counter, ObservableUpDownCounter)