diff --git a/CMakeLists.txt b/CMakeLists.txt index d1b1a384a8..f579efe431 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,17 +92,54 @@ if(VCPKG_CHAINLOAD_TOOLCHAIN_FILE) include("${VCPKG_CHAINLOAD_TOOLCHAIN_FILE}") endif() +option(WITH_ABI_VERSION_1 "ABI version 1" ON) +option(WITH_ABI_VERSION_2 "EXPERIMENTAL: ABI version 2 preview" OFF) + file(READ "${CMAKE_CURRENT_LIST_DIR}/api/include/opentelemetry/version.h" OPENTELEMETRY_CPP_HEADER_VERSION_H) -if(OPENTELEMETRY_CPP_HEADER_VERSION_H MATCHES - "OPENTELEMETRY_ABI_VERSION_NO[ \t\r\n]+\"?([0-9]+)\"?") - math(EXPR OPENTELEMETRY_ABI_VERSION_NO ${CMAKE_MATCH_1}) -else() + +# +# We do not want to have WITH_ABI_VERSION = "1" or "2", and instead prefer two +# distinct flags, WITH_ABI_VERSION_1 and WITH_ABI_VERSION_2. +# +# This allows: +# +# * to have a specific option description for each ABI +# * to mark experimental/stable/deprecated on flags, for clarity +# * to search for exact abi usage move easily, discouraging: +# +# * cmake -DWITH_ABI_VERSION=${ARG} +# +# While not supported, having distinct WITH_ABI_VERSION_1 and WITH_ABI_VERSION_2 +# flags also opens the possibility to support multiple ABI concurrently, should +# that become necessary. +# +if(WITH_ABI_VERSION_1 AND WITH_ABI_VERSION_2) + # + # Only one ABI is supported in a build. + # message( - FATAL_ERROR - "OPENTELEMETRY_ABI_VERSION_NO not found on ${CMAKE_CURRENT_LIST_DIR}/api/include/opentelemetry/version.h" - ) + FATAL_ERROR "Set either WITH_ABI_VERSION_1 or WITH_ABI_VERSION_2, not both") endif() + +if(WITH_ABI_VERSION_2) + set(OPENTELEMETRY_ABI_VERSION_NO "2") +elseif(WITH_ABI_VERSION_1) + set(OPENTELEMETRY_ABI_VERSION_NO "1") +else() + if(OPENTELEMETRY_CPP_HEADER_VERSION_H MATCHES + "OPENTELEMETRY_ABI_VERSION_NO[ \t\r\n]+\"?([0-9]+)\"?") + math(EXPR OPENTELEMETRY_ABI_VERSION_NO ${CMAKE_MATCH_1}) + else() + message( + FATAL_ERROR + "OPENTELEMETRY_ABI_VERSION_NO not found on ${CMAKE_CURRENT_LIST_DIR}/api/include/opentelemetry/version.h" + ) + endif() +endif() + +message(STATUS "OPENTELEMETRY_ABI_VERSION_NO=${OPENTELEMETRY_ABI_VERSION_NO}") + if(OPENTELEMETRY_CPP_HEADER_VERSION_H MATCHES "OPENTELEMETRY_VERSION[ \t\r\n]+\"?([^\"]+)\"?") set(OPENTELEMETRY_VERSION ${CMAKE_MATCH_1}) @@ -113,6 +150,8 @@ else() ) endif() +message(STATUS "OPENTELEMETRY_VERSION=${OPENTELEMETRY_VERSION}") + option(WITH_NO_DEPRECATED_CODE "Do not include deprecated code" OFF) option(WITH_STL "Whether to use Standard Library for C++ latest features" OFF) diff --git a/api/CMakeLists.txt b/api/CMakeLists.txt index 41b6997980..f5f1fbf897 100644 --- a/api/CMakeLists.txt +++ b/api/CMakeLists.txt @@ -94,6 +94,10 @@ if(WITH_REMOVE_METER_PREVIEW) INTERFACE ENABLE_REMOVE_METER_PREVIEW) endif() +target_compile_definitions( + opentelemetry_api + INTERFACE OPENTELEMETRY_ABI_VERSION_NO=${OPENTELEMETRY_ABI_VERSION_NO}) + # A better place should be in sdk, not api if(WITH_OTLP_HTTP_SSL_PREVIEW) target_compile_definitions(opentelemetry_api diff --git a/api/include/opentelemetry/version.h b/api/include/opentelemetry/version.h index c44e9a3691..fd88ffb383 100644 --- a/api/include/opentelemetry/version.h +++ b/api/include/opentelemetry/version.h @@ -6,7 +6,10 @@ #include "opentelemetry/common/macros.h" #include "opentelemetry/detail/preprocessor.h" -#define OPENTELEMETRY_ABI_VERSION_NO 1 +#ifndef OPENTELEMETRY_ABI_VERSION_NO +# define OPENTELEMETRY_ABI_VERSION_NO 1 +#endif + #define OPENTELEMETRY_VERSION "1.11.0" #define OPENTELEMETRY_VERSION_MAJOR 1 #define OPENTELEMETRY_VERSION_MINOR 11 diff --git a/docs/abi-version-policy.md b/docs/abi-version-policy.md new file mode 100644 index 0000000000..f3d66bb4f1 --- /dev/null +++ b/docs/abi-version-policy.md @@ -0,0 +1,529 @@ +# 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: + +```cpp +namespace opentelemetry +{ + namespace common + { + class OtelUtil + { + virtual void DoSomething() = 0; + }; + } +} +``` + +An application can be instrumented to use it: + +```cpp +opentelemetry::common::OtelUtil *p = ...; +p->DoSomething(); +``` + +The problem here is that the binary package produced during the build +contains references to symbols such as: + +```cpp +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: + +```cpp +namespace opentelemetry +{ + inline namespace v1 + { + namespace common + { + class OtelUtil + { + virtual void DoSomething() = 0; + }; + } + } +} +``` + +```cpp +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: + +```cpp +opentelemetry::v1::common::OtelUtil::DoSomething() +``` + +With the help of the `OPENTELEMETRY_BEGIN_NAMESPACE` macro, +the source code can be abstracted to: + +```cpp +#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: + +```cpp +#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: + +```cpp +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: + +```cpp +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: + +```cmake +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: + +```cmake +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: + +```cmake +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: + +```cmake +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: + +```cpp +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: + +```cpp +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: + +```cpp +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 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 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. + +```cpp +class MeterProvider final : public opentelemetry::metrics::MeterProvider +{ +public: + + /* ... */ + +#if OPENTELEMETRY_ABI_VERSION_NO >= 2 + nostd::shared_ptr 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 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 + +```cpp +nostd::shared_ptr 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 */ +} +```