opentelemetry-cpp/docs/abi-version-policy.md

13 KiB

Application Binary Interface (ABI) version policy

Goals

Instrumented applications

Once a given release of opentelemetry-cpp is published as stable, subsequent releases are expected to provide compatibility, to avoid disruption.

Compatibility at the source code level (API) is expected, so that no code change in an application already instrumented is required to adopt a newer release.

Also, compatibility at the binary level (ABI) is expected, so that an instrumented application already compiled against opentelemetry-cpp API headers from an older version, and distributed as a binary package, can be linked against the SDK library from a newer version.

In other words, once an application is instrumented using the opentelemetry-cpp API, adopting a newer version:

  • should not require source code changes,
  • should not require building and distributing a new package.

opentelemetry-cpp

The opentelemetry-cpp project itself needs to deliver fixes and features on a continual basis.

Reasons to change an API can be external:

  • new APIs added in the specification, for new features
  • changes in APIs in the specifications, extending existing features.

Changes can also be caused by internal issues:

  • fix technical design issues with an API (incorrect types used, missing parameters)

Regardless of the root cause for a change (bug or feature), changes to the existing APIs are unavoidable.

For the opentelemetry-cpp project to stay healthy, there must be a way to deliver ABI breaking fixes while preserving compatibility for users.

This is achieved with ABI versions.

Concept of ABI version

Inline namespaces

For the sake of illustration, let's consider a fictitious API such as:

namespace opentelemetry
{
  namespace common
  {
    class OtelUtil
    {
      virtual void DoSomething() = 0;
    };
  }
}

An application can be instrumented to use it:

opentelemetry::common::OtelUtil *p = ...;
p->DoSomething();

The problem here is that the binary package produced during the build contains references to symbols such as:

opentelemetry::common::OtelUtil::DoSomething()

Because all symbols are in the same opentelemetry:: namespace, it becomes impossible to deliver changes (new or different symbols) while at the same time not deliver changes (to preserve binary compatibility).

This is resolved by the use of inline namespaces in C++, leading to dedicated ABI versions.

Revised example:

namespace opentelemetry
{
  inline namespace v1
  {
    namespace common
    {
      class OtelUtil
      {
        virtual void DoSomething() = 0;
      };
    }
  }
}
opentelemetry::common::OtelUtil *p = ...;
p->DoSomething();

When building, the compiler translates opentelemetry::common to opentelemetry::v1::common

The symbols delivered by the opentelemetry library, and used by the instrumented application, are:

opentelemetry::v1::common::OtelUtil::DoSomething()

With the help of the OPENTELEMETRY_BEGIN_NAMESPACE macro, the source code can be abstracted to:

#define OPENTELEMETRY_ABI_VERSION_NO 1

OPENTELEMETRY_BEGIN_NAMESPACE
{
  namespace common
  {
    class OtelUtil
    {
      virtual void DoSomething() = 0;
    };
  }
}
OPENTELEMETRY_END_NAMESPACE

Adding a new API to the OtelUtil class is an ABI breaking change (the C++ virtual table is different).

This change can be delivered, but in a different namespace, which defines a different (extended) ABI:

#define OPENTELEMETRY_ABI_VERSION_NO 2

OPENTELEMETRY_BEGIN_NAMESPACE
{
  namespace common
  {
    class OtelUtil
    {
      virtual void DoSomething() = 0;
      virtual void DoSomethingMore() = 0;
    };
  }
}
OPENTELEMETRY_END_NAMESPACE

Compiling this declaration produce the following symbols:

opentelemetry::v2::common::OtelUtil::DoSomething()
opentelemetry::v2::common::OtelUtil::DoSomethingMore()

ABI version

By defining an inline namespace per ABI, it is possible to deliver an ABI v2 implementation independently of ABI v1, keeping v1 unchanged.

Code compiled against the v1 ABI can continue to link against a library providing v1 symbols, while code compiled against the v2 ABI can link against a library providing the v2 symbols.

An application, when building, can choose to:

  • either build against the v1 interface
  • or build against the v2 interface

Using v1 ensures compatibility, at the API and ABI level.

Using v2 allows to benefit from new features.

The choice is made by the application owner when building, and not by the opentelemetry-cpp library.

This, in essence, describes the technical 'ABI version' building block used to deliver breaking changes.

How to use this versioning feature, provided by the C++ language with inline namespaces, is described in the next section.

Versioning policy

Version scope

Due to dependencies between classes, having an ABI version per class is not viable.

For example, class TracerProvider depends on class Tracer, as it builds tracers.

If class Tracer comes in multiple versions, then class TracerProvider needs to also comes in multiple versions, and these versions are correlated:

  • v1::TracerProvider creates v1::Tracer instances
  • v2::TracerProvider creates v2::Tracer instances

The next logical scope is to consider ABI versions per signal.

This is not viable either, because of interdependencies between signals:

  • metrics can use traces in examplars
  • eventually, the trace implementation can emit internal metrics
  • all signals can use common utility classes

In conclusion, the scope of a version is the entire opentelemetry-cpp project.

Source code

The number of breaking change that affects an APIs is expected to be very low, with only specific methods affected directly.

It is very undesirable to:

  • create a different git branch per ABI version, in effect forking the entire code base
  • create a different header file per ABI version, in effect forking the entire include headers

just to handle a few modified APIs.

As a result, differences between ABI versions are handled using C preprocessor macros, in the few places where it is necessary.

For example:

OPENTELEMETRY_BEGIN_NAMESPACE
{
  namespace common
  {
    class OtelUtil
    {
      virtual void DoSomething() = 0;

#if OPENTELEMETRY_ABI_VERSION_NO >= 2
      // Added in ABI v2
      virtual void DoSomethingMore() = 0;
#endif

    };
  }
}
OPENTELEMETRY_END_NAMESPACE

The same source code, when compiled with different values of OPENTELEMETRY_ABI_VERSION_NO, produces the proper declarations for ABI v1 or ABI v2.

This solution reduces the maintenance burden on the source code itself.

Versions lifecycle

For a given ABI, the lifecycle is:

  • EXPERIMENTAL
  • STABLE
  • DEPRECATED
  • REMOVED

In the EXPERIMENTAL status, any change to the ABI can be implemented, without notice.

There are no compatibility guarantees. This status is meant as a preview, until the ABI is declared STABLE.

In the STABLE status, changes to the ABI are forbidden, to guarantee stability.

In the DEPRECATED status, the ABI is still functional and supported, but instrumented applications are encouraged to migrate to a newer ABI.

In the REMOVED status, the given ABI is no longer available.

The following sections describe the migration path from one ABI (v1) to the next (v2).

STABLE V1

In this state, only one ABI version is available, and it is closed to changes.

Instrumented applications are built against ABI v1 by default.

opentelemetry-cpp produces a library for ABI v1 by default.

Fixes introducing breaking changes can not be delivered.

This is the current status as of opentelemetry-cpp version 1.11.0

STABLE V1, EXPERIMENTAL V2

In this state, two ABI versions are available.

CMake offers the following options:

option(WITH_ABI_VERSION_1 "ABI version 1" ON)
option(WITH_ABI_VERSION_2 "EXPERIMENTAL: ABI version 2 preview" OFF)

Instrumented applications are built against ABI v1 by default, but may ask to use ABI v2 instead.

opentelemetry-cpp produces a library for stable ABI v1 by default, but can be configured to provide experimental ABI v2 instead.

Fixes introducing breaking changes can only be delivered in the experimental ABI v2.

STABLE V1, STABLE V2, EXPERIMENTAL V3

In this state, two stable ABI versions are available, the ABI offered by default is the conservative ABI v1.

Fixes introducing breaking changes can no longer be delivered in ABI v2, because it is declared stable. An experimental ABI v3 is created.

CMake offers the following options:

option(WITH_ABI_VERSION_1 "ABI version 1" ON)
option(WITH_ABI_VERSION_2 "ABI version 2" OFF)
option(WITH_ABI_VERSION_3 "EXPERIMENTAL: ABI version 3 preview" OFF)

Instrumented applications are built against stable ABI v1 by default, but may ask to use the now stable ABI v2 instead.

opentelemetry-cpp produces a library for ABI v1 by default, but can be configured to provide ABI v2 instead.

Fixes introducing breaking changes can only be delivered in the experimental ABI v3.

DEPRECATED V1, STABLE V2, EXPERIMENTAL V3

In this state, two stable ABI versions are available, the ABI offered by default is the newer ABI v2.

CMake offers the following options:

option(WITH_ABI_VERSION_1 "DEPRECATED: ABI version 1" OFF)
option(WITH_ABI_VERSION_2 "ABI version 2" ON)
option(WITH_ABI_VERSION_3 "EXPERIMENTAL: ABI version 3 preview" OFF)

Instrumented applications are built against stable ABI v2 by default, but may ask to use stable ABI v1 instead.

opentelemetry-cpp produces a library for ABI v2 by default, but can be configured to provide ABI v1 instead.

Fixes introducing breaking changes can only be delivered in the experimental ABI v3.

(REMOVED V1), STABLE V2, EXPERIMENTAL V3

In this state, the only stable ABI available is v2. ABI v1 is no longer supported.

CMake offers the following options:

option(WITH_ABI_VERSION_2 "ABI version 2" ON)
option(WITH_ABI_VERSION_3 "EXPERIMENTAL: ABI version 3 preview" OFF)

Instrumented applications and the opentelemetry-cpp library are build using ABI v2.

Now that ABI v1 no longer exists, the code:

OPENTELEMETRY_BEGIN_NAMESPACE
{
  namespace common
  {
    class OtelUtil
    {
      virtual void DoSomething() = 0;

#if OPENTELEMETRY_ABI_VERSION_NO >= 2
      // Added in ABI v2
      virtual void DoSomethingMore() = 0;
#endif

    };
  }
}
OPENTELEMETRY_END_NAMESPACE

can be simplified to:

OPENTELEMETRY_BEGIN_NAMESPACE
{
  namespace common
  {
    class OtelUtil
    {
      virtual void DoSomething() = 0;
      virtual void DoSomethingMore() = 0;
    };
  }
}
OPENTELEMETRY_END_NAMESPACE

Practical Example

Fixing issue #2033

The problem is to change the MeterProvider::GetMeter() prototype, to follow specification changes.

See the issue description for all details:

  • [Metrics API/SDK] Add InstrumentationScope attributes in MeterProvider::GetMeter() #2033

API change

In the API, class MeterProvider is changed as follows:

class MeterProvider
{
public:
  virtual ~MeterProvider() = default;
  /**
   * Gets or creates a named Meter instance.
   *
   * Optionally a version can be passed to create a named and versioned Meter
   * instance.
   */
#if OPENTELEMETRY_ABI_VERSION_NO >= 2
  virtual nostd::shared_ptr<Meter> GetMeter(nostd::string_view library_name,
                                            nostd::string_view library_version = "",
                                            nostd::string_view schema_url = "",
                                            const common::KeyValueIterable *attributes = nullptr) noexcept = 0;
#else
  virtual nostd::shared_ptr<Meter> GetMeter(nostd::string_view library_name,
                                            nostd::string_view library_version = "",
                                            nostd::string_view schema_url      = "") noexcept = 0;
#endif

  /* ... */
};

Note how the ABI changes, while the API stays compatible, requiring no code change in the caller when providing up to 3 parameters.

SDK change

In the SDK class declaration, implement the expected API.

class MeterProvider final : public opentelemetry::metrics::MeterProvider
{
public:

  /* ... */

#if OPENTELEMETRY_ABI_VERSION_NO >= 2
  nostd::shared_ptr<opentelemetry::metrics::Meter> GetMeter(
      nostd::string_view name,
      nostd::string_view version                                = "",
      nostd::string_view schema_url                             = "",
      const opentelemetry::common::KeyValueIterable *attributes = nullptr) noexcept override;
#else
  nostd::shared_ptr<opentelemetry::metrics::Meter> GetMeter(
      nostd::string_view name,
      nostd::string_view version    = "",
      nostd::string_view schema_url = "") noexcept override;
#endif

  /* ... */
};

In the SDK implementation:

  • either get the new parameters from the extended ABI v2 method
  • or provide default values for the old ABI v1 method
nostd::shared_ptr<metrics_api::Meter> MeterProvider::GetMeter(
    nostd::string_view name,
    nostd::string_view version,
    nostd::string_view schema_url
#if OPENTELEMETRY_ABI_VERSION_NO >= 2
    ,
    const opentelemetry::common::KeyValueIterable *attributes
#endif
    ) noexcept
{
#if OPENTELEMETRY_ABI_VERSION_NO < 2
  const opentelemetry::common::KeyValueIterable *attributes = nullptr;
#endif

  /* common implementation, use attributes */
}