Skip to content

Commit

Permalink
Add semver check to metrics API (#3357)
Browse files Browse the repository at this point in the history
  • Loading branch information
dyladan committed Oct 26, 2022
1 parent 4420402 commit d808e29
Show file tree
Hide file tree
Showing 8 changed files with 478 additions and 92 deletions.
3 changes: 3 additions & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to experimental packages in this project will be documented

### :boom: Breaking Change

* Add semver check to metrics API [#3357](https://github.com/open-telemetry/opentelemetry-js/pull/3357) @dyladan
* Previously API versions were only considered compatible if the API was exactly the same

### :rocket: (Enhancement)

* feat(metrics-sdk): Add tracing suppresing for Metrics Export [#3332](https://github.com/open-telemetry/opentelemetry-js/pull/3332) @hectorhdzg
Expand Down

This file was deleted.

32 changes: 7 additions & 25 deletions experimental/packages/opentelemetry-api-metrics/src/api/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
import { Meter, MeterOptions } from '../types/Meter';
import { MeterProvider } from '../types/MeterProvider';
import { NOOP_METER_PROVIDER } from '../NoopMeterProvider';
import {
API_BACKWARDS_COMPATIBILITY_VERSION,
GLOBAL_METRICS_API_KEY,
makeGetter,
_global,
} from './global-utils';
import { getGlobal, registerGlobal, unregisterGlobal } from '../internal/global-utils';

/**
* Singleton object which represents the entry point to the OpenTelemetry Metrics API
Expand All @@ -43,31 +38,18 @@ export class MetricsAPI {
}

/**
* Set the current global meter. Returns the initialized global meter provider.
* Set the current global meter provider.
* Returns true if the meter provider was successfully registered, else false.
*/
public setGlobalMeterProvider(provider: MeterProvider): MeterProvider {
if (_global[GLOBAL_METRICS_API_KEY]) {
// global meter provider has already been set
return this.getMeterProvider();
}

_global[GLOBAL_METRICS_API_KEY] = makeGetter(
API_BACKWARDS_COMPATIBILITY_VERSION,
provider,
NOOP_METER_PROVIDER
);

return provider;
public setGlobalMeterProvider(provider: MeterProvider): boolean {
return registerGlobal('metrics', provider);
}

/**
* Returns the global meter provider.
*/
public getMeterProvider(): MeterProvider {
return (
_global[GLOBAL_METRICS_API_KEY]?.(API_BACKWARDS_COMPATIBILITY_VERSION) ??
NOOP_METER_PROVIDER
);
return getGlobal('metrics') || NOOP_METER_PROVIDER;
}

/**
Expand All @@ -79,6 +61,6 @@ export class MetricsAPI {

/** Remove the global meter provider */
public disable(): void {
delete _global[GLOBAL_METRICS_API_KEY];
unregisterGlobal('metrics');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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
*
* https://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.
*/

import { diag } from '@opentelemetry/api';
import { _globalThis } from '../platform';
import { MeterProvider } from '../types/MeterProvider';
import { VERSION } from '../version';
import { isCompatible } from './semver';

const major = VERSION.split('.')[0];
const GLOBAL_OPENTELEMETRY_METRICS_API_KEY = Symbol.for(
`opentelemetry.js.api.metrics.${major}`
);

const _global = _globalThis as OTelGlobal;

export function registerGlobal<Type extends keyof OTelGlobalAPI>(
type: Type,
instance: OTelGlobalAPI[Type],
allowOverride = false
): boolean {
const api = (_global[GLOBAL_OPENTELEMETRY_METRICS_API_KEY] = _global[
GLOBAL_OPENTELEMETRY_METRICS_API_KEY
] ?? {
version: VERSION,
});

if (!allowOverride && api[type]) {
// already registered an API of this type
const err = new Error(
`@opentelemetry/api: Attempted duplicate registration of API: ${type}`
);
diag.error(err.stack || err.message);
return false;
}

if (api.version !== VERSION) {
// All registered APIs must be of the same version exactly
const err = new Error(
'@opentelemetry/api: All API registration versions must match'
);
diag.error(err.stack || err.message);
return false;
}

api[type] = instance;
diag.debug(
`@opentelemetry/api: Registered a global for ${type} v${VERSION}.`
);

return true;
}

export function getGlobal<Type extends keyof OTelGlobalAPI>(
type: Type
): OTelGlobalAPI[Type] | undefined {
const globalVersion = _global[GLOBAL_OPENTELEMETRY_METRICS_API_KEY]?.version;
if (!globalVersion || !isCompatible(globalVersion)) {
return;
}
return _global[GLOBAL_OPENTELEMETRY_METRICS_API_KEY]?.[type];
}

export function unregisterGlobal(type: keyof OTelGlobalAPI) {
diag.debug(
`@opentelemetry/api-metrics: Unregistering a global for ${type} v${VERSION}.`
);
const api = _global[GLOBAL_OPENTELEMETRY_METRICS_API_KEY];

if (api) {
delete api[type];
}
}

type OTelGlobal = {
[GLOBAL_OPENTELEMETRY_METRICS_API_KEY]?: OTelGlobalAPI;
};

type OTelGlobalAPI = {
version: string;

metrics?: MeterProvider;
};
140 changes: 140 additions & 0 deletions experimental/packages/opentelemetry-api-metrics/src/internal/semver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* 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
*
* https://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.
*/

import { VERSION } from '../version';

const re = /^(\d+)\.(\d+)\.(\d+)(-(.+))?$/;

/**
* Create a function to test an API version to see if it is compatible with the provided ownVersion.
*
* The returned function has the following semantics:
* - Exact match is always compatible
* - Major versions must match exactly
* - 1.x package cannot use global 2.x package
* - 2.x package cannot use global 1.x package
* - The minor version of the API module requesting access to the global API must be less than or equal to the minor version of this API
* - 1.3 package may use 1.4 global because the later global contains all functions 1.3 expects
* - 1.4 package may NOT use 1.3 global because it may try to call functions which don't exist on 1.3
* - If the major version is 0, the minor version is treated as the major and the patch is treated as the minor
* - Patch and build tag differences are not considered at this time
*
* @param ownVersion version which should be checked against
*/
export function _makeCompatibilityCheck(
ownVersion: string
): (globalVersion: string) => boolean {
const acceptedVersions = new Set<string>([ownVersion]);
const rejectedVersions = new Set<string>();

const myVersionMatch = ownVersion.match(re);
if (!myVersionMatch) {
// we cannot guarantee compatibility so we always return noop
return () => false;
}

const ownVersionParsed = {
major: +myVersionMatch[1],
minor: +myVersionMatch[2],
patch: +myVersionMatch[3],
prerelease: myVersionMatch[4],
};

// if ownVersion has a prerelease tag, versions must match exactly
if (ownVersionParsed.prerelease != null) {
return function isExactmatch(globalVersion: string): boolean {
return globalVersion === ownVersion;
};
}

function _reject(v: string) {
rejectedVersions.add(v);
return false;
}

function _accept(v: string) {
acceptedVersions.add(v);
return true;
}

return function isCompatible(globalVersion: string): boolean {
if (acceptedVersions.has(globalVersion)) {
return true;
}

if (rejectedVersions.has(globalVersion)) {
return false;
}

const globalVersionMatch = globalVersion.match(re);
if (!globalVersionMatch) {
// cannot parse other version
// we cannot guarantee compatibility so we always noop
return _reject(globalVersion);
}

const globalVersionParsed = {
major: +globalVersionMatch[1],
minor: +globalVersionMatch[2],
patch: +globalVersionMatch[3],
prerelease: globalVersionMatch[4],
};

// if globalVersion has a prerelease tag, versions must match exactly
if (globalVersionParsed.prerelease != null) {
return _reject(globalVersion);
}

// major versions must match
if (ownVersionParsed.major !== globalVersionParsed.major) {
return _reject(globalVersion);
}

if (ownVersionParsed.major === 0) {
if (
ownVersionParsed.minor === globalVersionParsed.minor &&
ownVersionParsed.patch <= globalVersionParsed.patch
) {
return _accept(globalVersion);
}

return _reject(globalVersion);
}

if (ownVersionParsed.minor <= globalVersionParsed.minor) {
return _accept(globalVersion);
}

return _reject(globalVersion);
};
}

/**
* Test an API version to see if it is compatible with this API.
*
* - Exact match is always compatible
* - Major versions must match exactly
* - 1.x package cannot use global 2.x package
* - 2.x package cannot use global 1.x package
* - The minor version of the API module requesting access to the global API must be less than or equal to the minor version of this API
* - 1.3 package may use 1.4 global because the later global contains all functions 1.3 expects
* - 1.4 package may NOT use 1.3 global because it may try to call functions which don't exist on 1.3
* - If the major version is 0, the minor version is treated as the major and the patch is treated as the minor
* - Patch and build tag differences are not considered at this time
*
* @param version version of the API requesting an instance of the global API
*/
export const isCompatible = _makeCompatibilityCheck(VERSION);

0 comments on commit d808e29

Please sign in to comment.