feat(core): add more scalable replacements for getEnv(), getEnvWithoutDefaults() (#5443)

This commit is contained in:
Marc Pichler 2025-02-12 11:24:01 +01:00 committed by GitHub
parent 9feaee3ff5
commit 1ed613c248
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 374 additions and 32 deletions

View File

@ -98,6 +98,7 @@ For semantic convention package changes, see the [semconv CHANGELOG](packages/se
* feat(sdk-trace-web): do not throw when passing extra options [#5357](https://github.com/open-telemetry/opentelemetry-js/pull/5357) @pichlermarc
* `WebTracerProvider` constructor now does not throw anymore when `contextManager` or `propagator` are passed as extra options to the constructor
* feat(sdk-trace-base): add stack trace warning to debug instrumentation [#5363](https://github.com/open-telemetry/opentelemetry-js/pull/5363) @neilfordyce
* feat(core): add more scalable replacements for getEnv(), getEnvWithoutDefaults() [#5443](https://github.com/open-telemetry/opentelemetry-js/pull/5443) @pichlermarc
### :bug: (Bug Fix)

View File

@ -1,11 +1,11 @@
/*!
/*
* 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
* 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,
@ -17,8 +17,12 @@
const karmaWebpackConfig = require('../../karma.webpack');
const karmaBaseConfig = require('../../karma.base');
module.exports = (config) => {
config.set(Object.assign({}, karmaBaseConfig, {
webpack: karmaWebpackConfig,
}))
module.exports = config => {
config.set(
Object.assign({}, karmaBaseConfig, {
webpack: karmaWebpackConfig,
files: ['test/browser/index-webpack.ts'],
preprocessors: { 'test/browser/index-webpack.ts': ['webpack'] },
})
);
};

View File

@ -17,7 +17,7 @@
"prepublishOnly": "npm run compile",
"compile": "tsc --build tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
"clean": "tsc --build --clean tsconfig.json tsconfig.esm.json tsconfig.esnext.json",
"test": "nyc mocha test/**/*.test.ts --exclude 'test/platform/browser/**/*.ts'",
"test": "nyc mocha 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'",
"test:browser": "karma start --single-run",
"tdd": "npm run tdd:node",
"tdd:node": "npm run test -- --watch-extensions ts --watch",

View File

@ -44,6 +44,10 @@ export {
_globalThis,
getEnv,
getEnvWithoutDefaults,
getStringFromEnv,
getBooleanFromEnv,
getNumberFromEnv,
getStringListFromEnv,
otperformance,
unrefTimer,
} from './platform';

View File

@ -32,6 +32,22 @@ export function getEnv(): Required<ENVIRONMENT> {
return Object.assign({}, DEFAULT_ENVIRONMENT, globalEnv);
}
export function getStringFromEnv(_: string): string | undefined {
return undefined;
}
export function getBooleanFromEnv(_: string): boolean | undefined {
return undefined;
}
export function getNumberFromEnv(_: string): boolean | undefined {
return undefined;
}
export function getStringListFromEnv(_: string): string[] | undefined {
return undefined;
}
export function getEnvWithoutDefaults(): ENVIRONMENT {
return parseEnvironment(_globalThis as typeof globalThis & RAW_ENVIRONMENT);
}

View File

@ -14,7 +14,14 @@
* limitations under the License.
*/
export { getEnvWithoutDefaults, getEnv } from './environment';
export {
getEnvWithoutDefaults,
getEnv,
getStringFromEnv,
getBooleanFromEnv,
getNumberFromEnv,
getStringListFromEnv,
} from './environment';
export { _globalThis } from './globalThis';
export { otperformance } from './performance';
export { SDK_INFO } from './sdk-info';

View File

@ -20,4 +20,8 @@ export {
getEnvWithoutDefaults,
otperformance,
unrefTimer,
getBooleanFromEnv,
getStringFromEnv,
getNumberFromEnv,
getStringListFromEnv,
} from './node';

View File

@ -20,6 +20,8 @@ import {
RAW_ENVIRONMENT,
parseEnvironment,
} from '../../utils/environment';
import { diag } from '@opentelemetry/api';
import { inspect } from 'util';
/**
* Gets the environment variables
@ -29,6 +31,93 @@ export function getEnv(): Required<ENVIRONMENT> {
return Object.assign({}, DEFAULT_ENVIRONMENT, processEnv);
}
/**
* Retrieves a number from an environment variable.
* - Returns `undefined` if the environment variable is empty, unset, contains only whitespace, or is not a number.
* - Returns a number in all other cases.
*
* @param {string} key - The name of the environment variable to retrieve.
* @returns {number | undefined} - The number value or `undefined`.
*/
export function getNumberFromEnv(key: string): number | undefined {
const raw = process.env[key];
if (raw == null || raw.trim() === '') {
return undefined;
}
const value = Number(raw);
if (isNaN(value)) {
diag.warn(
`Unknown value ${inspect(raw)} for ${key}, expected a number, using defaults`
);
return undefined;
}
return value;
}
/**
* Retrieves a string from an environment variable.
* - Returns `undefined` if the environment variable is empty, unset, or contains only whitespace.
*
* @param {string} key - The name of the environment variable to retrieve.
* @returns {string | undefined} - The string value or `undefined`.
*/
export function getStringFromEnv(key: string): string | undefined {
const raw = process.env[key];
if (raw == null || raw.trim() === '') {
return undefined;
}
return raw;
}
/**
* Retrieves a boolean value from an environment variable.
* - Trims leading and trailing whitespace and ignores casing.
* - Returns `false` if the environment variable is empty, unset, or contains only whitespace.
* - Returns `false` for strings that cannot be mapped to a boolean.
*
* @param {string} key - The name of the environment variable to retrieve.
* @returns {boolean} - The boolean value or `false` if the environment variable is unset empty, unset, or contains only whitespace.
*/
export function getBooleanFromEnv(key: string): boolean {
const raw = process.env[key]?.trim().toLowerCase();
if (raw == null || raw === '') {
// NOTE: falling back to `false` instead of `undefined` as required by the specification.
// If you have a use-case that requires `undefined`, consider using `getStringFromEnv()` and applying the necessary
// normalizations in the consuming code.
return false;
}
if (raw === 'true') {
return true;
} else if (raw === 'false') {
return false;
} else {
diag.warn(
`Unknown value ${inspect(raw)} for ${key}, expected 'true' or 'false', falling back to 'false' (default)`
);
return false;
}
}
/**
* Retrieves a list of strings from an environment variable.
* - Uses ',' as the delimiter.
* - Trims leading and trailing whitespace from each entry.
* - Excludes empty entries.
* - Returns `undefined` if the environment variable is empty or contains only whitespace.
* - Returns an empty array if all entries are empty or whitespace.
*
* @param {string} key - The name of the environment variable to retrieve.
* @returns {string[] | undefined} - The list of strings or `undefined`.
*/
export function getStringListFromEnv(key: string): string[] | undefined {
return getStringFromEnv(key)
?.split(',')
.map(v => v.trim())
.filter(s => s !== '');
}
export function getEnvWithoutDefaults(): ENVIRONMENT {
return parseEnvironment(process.env as RAW_ENVIRONMENT);
}

View File

@ -14,7 +14,14 @@
* limitations under the License.
*/
export { getEnvWithoutDefaults, getEnv } from './environment';
export {
getEnvWithoutDefaults,
getEnv,
getStringFromEnv,
getBooleanFromEnv,
getNumberFromEnv,
getStringListFromEnv,
} from './environment';
export { _globalThis } from './globalThis';
export { otperformance } from './performance';
export { SDK_INFO } from './sdk-info';

View File

@ -15,7 +15,7 @@
*/
import * as assert from 'assert';
import { getEnv } from '../../../src/platform/browser/environment';
import { getEnv } from '../../src/platform/browser/environment';
describe('getEnv', () => {
it('get environments variables in a browser', () => {

View File

@ -13,7 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const testsContextCommon = require.context('.', true, /test$/);
const testsContext = require.context('../browser', true, /test$/);
testsContext.keys().forEach(testsContext);
const testsContextCommon = require.context('../common', true, /test$/);
testsContextCommon.keys().forEach(testsContextCommon);
const srcContext = require.context('.', true, /src$/);

View File

@ -24,8 +24,8 @@ import {
} from '@opentelemetry/api';
import { ROOT_CONTEXT } from '@opentelemetry/api';
import * as assert from 'assert';
import { W3CBaggagePropagator } from '../../src/baggage/propagation/W3CBaggagePropagator';
import { BAGGAGE_HEADER } from '../../src/baggage/constants';
import { W3CBaggagePropagator } from '../../../src/baggage/propagation/W3CBaggagePropagator';
import { BAGGAGE_HEADER } from '../../../src/baggage/constants';
describe('W3CBaggagePropagator', () => {
const httpBaggagePropagator = new W3CBaggagePropagator();

View File

@ -16,9 +16,9 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { ExportResult, ExportResultCode } from '../../src';
import * as suppress from '../../src/trace/suppress-tracing';
import { _export } from '../../src/internal/exporter';
import { ExportResult, ExportResultCode } from '../../../src';
import * as suppress from '../../../src/trace/suppress-tracing';
import { _export } from '../../../src/internal/exporter';
describe('exporter', () => {
const sandbox = sinon.createSandbox();

View File

@ -15,7 +15,7 @@
*/
import * as assert from 'assert';
import { validateKey, validateValue } from '../../src/internal/validators';
import { validateKey, validateValue } from '../../../src/internal/validators';
describe('validators', () => {
describe('validateKey', () => {

View File

@ -25,12 +25,12 @@ import {
} from '@opentelemetry/api';
import { Context, ROOT_CONTEXT } from '@opentelemetry/api';
import * as assert from 'assert';
import { CompositePropagator, W3CTraceContextPropagator } from '../../src';
import { CompositePropagator, W3CTraceContextPropagator } from '../../../src';
import {
TRACE_PARENT_HEADER,
TRACE_STATE_HEADER,
} from '../../src/trace/W3CTraceContextPropagator';
import { TraceState } from '../../src/trace/TraceState';
} from '../../../src/trace/W3CTraceContextPropagator';
import { TraceState } from '../../../src/trace/TraceState';
class DummyPropagator implements TextMapPropagator {
inject(context: Context, carrier: any, setter: TextMapSetter<any>): void {

View File

@ -29,9 +29,9 @@ import {
W3CTraceContextPropagator,
TRACE_PARENT_HEADER,
TRACE_STATE_HEADER,
} from '../../src/trace/W3CTraceContextPropagator';
import { suppressTracing } from '../../src/trace/suppress-tracing';
import { TraceState } from '../../src/trace/TraceState';
} from '../../../src/trace/W3CTraceContextPropagator';
import { suppressTracing } from '../../../src/trace/suppress-tracing';
import { TraceState } from '../../../src/trace/TraceState';
describe('W3CTraceContextPropagator', () => {
const httpTraceContext = new W3CTraceContextPropagator();

View File

@ -15,7 +15,7 @@
*/
import * as assert from 'assert';
import { TraceState } from '../../src/trace/TraceState';
import { TraceState } from '../../../src/trace/TraceState';
describe('TraceState', () => {
describe('.serialize()', () => {

View File

@ -16,8 +16,8 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { BindOnceFuture } from '../../src';
import { assertRejects } from '../test-utils';
import { BindOnceFuture } from '../../../src';
import { assertRejects } from '../../test-utils';
describe('callback', () => {
describe('BindOnceFuture', () => {

View File

@ -14,12 +14,12 @@
* limitations under the License.
*/
import { getEnv } from '../../src/platform';
import { getEnv } from '../../../src/platform';
import {
DEFAULT_ENVIRONMENT,
ENVIRONMENT,
RAW_ENVIRONMENT,
} from '../../src/utils/environment';
} from '../../../src/utils/environment';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { DiagLogLevel } from '@opentelemetry/api';

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import * as assert from 'assert';
import { merge } from '../../src/utils/merge';
import { merge } from '../../../src/utils/merge';
const tests: TestResult[] = [];

View File

@ -15,8 +15,8 @@
*/
import * as assert from 'assert';
import { Deferred } from '../../src/utils/promise';
import { assertRejects } from '../test-utils';
import { Deferred } from '../../../src/utils/promise';
import { assertRejects } from '../../test-utils';
describe('promise', () => {
describe('Deferred', () => {

View File

@ -16,7 +16,7 @@
import * as assert from 'assert';
import { isUrlIgnored } from '../../src';
import { isUrlIgnored } from '../../../src';
const urlIgnored = 'url should be ignored';
const urlNotIgnored = 'url should NOT be ignored';

View File

@ -0,0 +1,207 @@
/*
* 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 {
getStringFromEnv,
getNumberFromEnv,
getStringListFromEnv,
getBooleanFromEnv,
} from '../../src';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { diag } from '@opentelemetry/api';
describe('environment utility functions', function () {
describe('getStringFromEnv', function () {
afterEach(function () {
delete process.env.FOO;
});
it('should treat empty string as undefined', function () {
process.env.FOO = '';
assert.strictEqual(getStringFromEnv('FOO'), undefined);
process.env.FOO = ' ';
assert.strictEqual(getStringFromEnv('FOO'), undefined);
});
it('should treat not defined as undefined', function () {
delete process.env.FOO;
assert.strictEqual(getStringFromEnv('FOO'), undefined);
});
it('should not trim extra whitespace', function () {
process.env.FOO = 'test-string ';
assert.strictEqual(getStringFromEnv('FOO'), 'test-string ');
process.env.FOO = ' test-string';
assert.strictEqual(getStringFromEnv('FOO'), ' test-string');
process.env.FOO = ' test-string ';
assert.strictEqual(getStringFromEnv('FOO'), ' test-string ');
});
it('should extract string from env var', function () {
process.env.FOO = 'test-string';
assert.strictEqual(getStringFromEnv('FOO'), 'test-string');
});
it('should retain casing', function () {
process.env.FOO = 'TeSt StRINg';
assert.strictEqual(getStringFromEnv('FOO'), 'TeSt StRINg');
});
});
describe('getNumberFromEnv', function () {
afterEach(function () {
delete process.env.FOO;
sinon.restore();
});
it('should treat empty string as undefined', function () {
process.env.FOO = '';
assert.strictEqual(getNumberFromEnv('FOO'), undefined);
process.env.FOO = ' ';
assert.strictEqual(getNumberFromEnv('FOO'), undefined);
});
it('should treat not defined as undefined', function () {
delete process.env.FOO;
assert.strictEqual(getNumberFromEnv('FOO'), undefined);
});
it('should trim extra whitespace', function () {
process.env.FOO = '1.234 ';
assert.strictEqual(getNumberFromEnv('FOO'), 1.234);
process.env.FOO = ' 1.234';
assert.strictEqual(getNumberFromEnv('FOO'), 1.234);
process.env.FOO = ' 1.234 ';
assert.strictEqual(getNumberFromEnv('FOO'), 1.234);
});
it('should extract integer from env var', function () {
process.env.FOO = String(42);
assert.strictEqual(getNumberFromEnv('FOO'), 42);
});
it('should extract double from env var', function () {
process.env.FOO = String(1.234);
assert.strictEqual(getNumberFromEnv('FOO'), 1.234);
});
it('should extract infinity from env var', function () {
process.env.FOO = String(Infinity);
assert.strictEqual(getNumberFromEnv('FOO'), Infinity);
});
it('should treat NaN as undefined', function () {
process.env.FOO = String(NaN);
assert.strictEqual(getNumberFromEnv('FOO'), undefined);
});
it('should ignore bogus data and warn', function () {
const warnStub = sinon.stub(diag, 'warn');
process.env.FOO = 'forty-two';
assert.strictEqual(getNumberFromEnv('FOO'), undefined);
sinon.assert.calledOnceWithMatch(warnStub, 'Unknown value');
});
});
describe('getStringListFromEnv', function () {
afterEach(function () {
delete process.env.FOO;
});
it('should treat empty string as undefined', function () {
process.env.FOO = '';
assert.strictEqual(getStringListFromEnv('FOO'), undefined);
});
it('should treat not defined as undefined', function () {
delete process.env.FOO;
assert.strictEqual(getStringListFromEnv('FOO'), undefined);
});
it('should trim extra whitespace', function () {
process.env.FOO = ' foo, bar, ';
assert.deepStrictEqual(getStringListFromEnv('FOO'), ['foo', 'bar']);
});
it('should extract list from env var', function () {
process.env.FOO = 'foo,bar,baz';
assert.deepStrictEqual(getStringListFromEnv('FOO'), [
'foo',
'bar',
'baz',
]);
});
it('should skip empty entries', function () {
process.env.FOO = ' ,undefined,,null, ,empty,';
assert.deepStrictEqual(getStringListFromEnv('FOO'), [
'undefined',
'null',
'empty',
]);
});
it('should retain casing', function () {
process.env.FOO = 'fOo,BaR';
assert.deepStrictEqual(getStringListFromEnv('FOO'), ['fOo', 'BaR']);
});
});
describe('getBooleanFromEnv', function () {
afterEach(function () {
delete process.env.FOO;
sinon.restore();
});
it('should treat empty string as false', function () {
process.env.FOO = '';
assert.strictEqual(getBooleanFromEnv('FOO'), false);
process.env.FOO = ' ';
assert.strictEqual(getBooleanFromEnv('FOO'), false);
});
it('should treat not defined as false', function () {
delete process.env.FOO;
assert.strictEqual(getBooleanFromEnv('FOO'), false);
});
it('should trim extra whitespace', function () {
process.env.FOO = ' true';
assert.strictEqual(getBooleanFromEnv('FOO'), true);
process.env.FOO = 'false ';
assert.strictEqual(getBooleanFromEnv('FOO'), false);
process.env.FOO = ' true ';
assert.strictEqual(getBooleanFromEnv('FOO'), true);
});
it('should ignore casing', function () {
process.env.FOO = 'tRUe';
assert.strictEqual(getBooleanFromEnv('FOO'), true);
process.env.FOO = 'TRUE';
assert.strictEqual(getBooleanFromEnv('FOO'), true);
process.env.FOO = 'FaLsE';
assert.strictEqual(getBooleanFromEnv('FOO'), false);
});
it('should ignore bogus data and warn', function () {
const warnStub = sinon.stub(diag, 'warn');
process.env.FOO = 'trueFALSE';
assert.strictEqual(getBooleanFromEnv('FOO'), false);
sinon.assert.calledOnceWithMatch(warnStub, 'Unknown value');
});
});
});