diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 15ee9866d78..52b359459aa 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -13,6 +13,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * feat(otlp-transformer): add span flags support for isRemote property [#5910](https://github.com/open-telemetry/opentelemetry-js/pull/5910) @nikhilmantri0902 * feat(sampler-composite): Added experimental implementations of draft composite sampling spec [#5839](https://github.com/open-telemetry/opentelemetry-js/pull/5839) @anuraaga * feat(opentelemetry-configuration): add more attributes to config model [#5826](https://github.com/open-telemetry/opentelemetry-js/pull/5826) @maryliag +* feat(opentelemetry-configuration): Parse of Configuration File [#5875](https://github.com/open-telemetry/opentelemetry-js/pull/5875) @maryliag ### :bug: Bug Fixes diff --git a/experimental/packages/opentelemetry-configuration/package.json b/experimental/packages/opentelemetry-configuration/package.json index 3e857c0b560..04cda7ff471 100644 --- a/experimental/packages/opentelemetry-configuration/package.json +++ b/experimental/packages/opentelemetry-configuration/package.json @@ -41,7 +41,8 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "2.1.0" + "@opentelemetry/core": "2.1.0", + "yaml": "^1.10.2" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/experimental/packages/opentelemetry-configuration/src/EnvironmentConfigProvider.ts b/experimental/packages/opentelemetry-configuration/src/EnvironmentConfigProvider.ts index 4f1c960fe0d..a9ad8afbd4d 100644 --- a/experimental/packages/opentelemetry-configuration/src/EnvironmentConfigProvider.ts +++ b/experimental/packages/opentelemetry-configuration/src/EnvironmentConfigProvider.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { DiagLogLevel } from '@opentelemetry/api'; import { ConfigurationModel, initializeDefaultConfiguration, @@ -36,12 +35,11 @@ export class EnvironmentConfigProvider implements ConfigProvider { constructor() { this._config = initializeDefaultConfiguration(); - this._config.disable = getBooleanFromEnv('OTEL_SDK_DISABLED'); + this._config.disabled = getBooleanFromEnv('OTEL_SDK_DISABLED'); - const logLevel = getStringFromEnv('OTEL_LOG_LEVEL'); + const logLevel = diagLogLevelFromString(getStringFromEnv('OTEL_LOG_LEVEL')); if (logLevel) { - this._config.log_level = - diagLogLevelFromString(logLevel) ?? DiagLogLevel.INFO; + this._config.log_level = logLevel; } const nodeResourceDetectors = getStringListFromEnv( diff --git a/experimental/packages/opentelemetry-configuration/src/FileConfigProvider.ts b/experimental/packages/opentelemetry-configuration/src/FileConfigProvider.ts index 58b3af0093e..ebcd47436a9 100644 --- a/experimental/packages/opentelemetry-configuration/src/FileConfigProvider.ts +++ b/experimental/packages/opentelemetry-configuration/src/FileConfigProvider.ts @@ -14,19 +14,27 @@ * limitations under the License. */ -import { getStringFromEnv } from '@opentelemetry/core'; +import { diagLogLevelFromString, getStringFromEnv } from '@opentelemetry/core'; import { + ConfigAttributes, ConfigurationModel, initializeDefaultConfiguration, } from './configModel'; import { ConfigProvider } from './IConfigProvider'; import * as fs from 'fs'; +import * as yaml from 'yaml'; +import { + getBooleanFromConfigFile, + getNumberFromConfigFile, + getStringFromConfigFile, +} from './utils'; export class FileConfigProvider implements ConfigProvider { private _config: ConfigurationModel; constructor() { this._config = initializeDefaultConfiguration(); + parseConfigFile(this._config); } getInstrumentationConfig(): ConfigurationModel { @@ -37,7 +45,10 @@ export class FileConfigProvider implements ConfigProvider { export function hasValidConfigFile(): boolean { const configFile = getStringFromEnv('OTEL_EXPERIMENTAL_CONFIG_FILE'); if (configFile) { - if (!configFile.endsWith('.yaml') || !fs.existsSync(configFile)) { + if ( + !(configFile.endsWith('.yaml') || configFile.endsWith('.yml')) || + !fs.existsSync(configFile) + ) { throw new Error( `Config file ${configFile} set on OTEL_EXPERIMENTAL_CONFIG_FILE is not valid` ); @@ -46,3 +57,64 @@ export function hasValidConfigFile(): boolean { } return false; } + +function parseConfigFile(config: ConfigurationModel) { + const supportedFileVersions = ['1.0-rc.1']; + const configFile = getStringFromEnv('OTEL_EXPERIMENTAL_CONFIG_FILE') || ''; + const file = fs.readFileSync(configFile, 'utf8'); + const parsedContent = yaml.parse(file); + + if ( + parsedContent['file_format'] && + supportedFileVersions.includes(parsedContent['file_format']) + ) { + const disabled = getBooleanFromConfigFile(parsedContent['disabled']); + if (disabled || disabled === false) { + config.disabled = disabled; + } + + const logLevel = getNumberFromConfigFile( + diagLogLevelFromString(parsedContent['log_level']) + ); + if (logLevel) { + config.log_level = logLevel; + } + + const attrList = getStringFromConfigFile( + parsedContent['resource']?.['attributes_list'] + ); + if (attrList) { + config.resource.attributes_list = attrList; + } + + const schemaUrl = getStringFromConfigFile( + parsedContent['resource']?.['schema_url'] + ); + if (schemaUrl) { + config.resource.schema_url = schemaUrl; + } + + setResourceAttributes(config, parsedContent['resource']?.['attributes']); + } else { + throw new Error( + `Unsupported File Format: ${parsedContent['file_format']}. It must be one of the following: ${supportedFileVersions}` + ); + } +} + +function setResourceAttributes( + config: ConfigurationModel, + attributes: ConfigAttributes[] +) { + if (attributes) { + config.resource.attributes = []; + for (let i = 0; i < attributes.length; i++) { + const att = attributes[i]; + config.resource.attributes.push({ + name: getStringFromConfigFile(att['name']) ?? '', + value: att['value'], + type: att['type'] ?? 'string', + }); + } + } +} diff --git a/experimental/packages/opentelemetry-configuration/src/configModel.ts b/experimental/packages/opentelemetry-configuration/src/configModel.ts index d697675bf61..990a7618541 100644 --- a/experimental/packages/opentelemetry-configuration/src/configModel.ts +++ b/experimental/packages/opentelemetry-configuration/src/configModel.ts @@ -21,7 +21,7 @@ export interface ConfigurationModel { * Configure if the SDK is disabled or not. * If omitted or null, false is used. */ - disable: boolean; + disabled: boolean; /** * Configure the log level of the internal logger used by the SDK. @@ -70,7 +70,7 @@ export interface ConfigurationModel { export function initializeDefaultConfiguration(): ConfigurationModel { const config: ConfigurationModel = { - disable: false, + disabled: false, log_level: DiagLogLevel.INFO, node_resource_detectors: ['all'], resource: {}, @@ -163,7 +163,7 @@ export function initializeDefaultConfiguration(): ConfigurationModel { export interface ConfigAttributes { name: string; value: string | boolean | number | string[] | boolean[] | number[]; - type?: + type: | 'string' | 'bool' | 'int' diff --git a/experimental/packages/opentelemetry-configuration/src/utils.ts b/experimental/packages/opentelemetry-configuration/src/utils.ts new file mode 100644 index 00000000000..56ad4a41196 --- /dev/null +++ b/experimental/packages/opentelemetry-configuration/src/utils.ts @@ -0,0 +1,79 @@ +/* + * 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 { inspect } from 'util'; + +/** + * Retrieves a boolean value from a configuration file parameter. + * - Trims leading and trailing whitespace and ignores casing. + * - Returns `undefined` if the value is empty, unset, or contains only whitespace. + * - Returns `undefined` and a warning for values that cannot be mapped to a boolean. + * + * @param {unknown} value - The value from the config file. + * @returns {boolean} - The boolean value or `false` if the environment variable is unset empty, unset, or contains only whitespace. + */ +export function getBooleanFromConfigFile(value: unknown): boolean | undefined { + const raw = String(value)?.trim().toLowerCase(); + if (raw === 'true') { + return true; + } else if (raw === 'false') { + return false; + } else if (raw == null || raw === '') { + return undefined; + } else { + diag.warn(`Unknown value ${inspect(raw)}, expected 'true' or 'false'`); + return undefined; + } +} + +/** + * Retrieves a number from a configuration file parameter. + * - Returns `undefined` if the environment variable is empty, unset, or contains only whitespace. + * - Returns `undefined` and a warning if is not a number. + * - Returns a number in all other cases. + * + * @param {unknown} value - The value from the config file. + * @returns {number | undefined} - The number value or `undefined`. + */ +export function getNumberFromConfigFile(value: unknown): number | undefined { + const raw = String(value)?.trim(); + if (raw == null || raw.trim() === '') { + return undefined; + } + + const n = Number(raw); + if (isNaN(n)) { + diag.warn(`Unknown value ${inspect(raw)}, expected a number`); + return undefined; + } + + return n; +} + +/** + * Retrieves a string from a configuration file parameter. + * - Returns `undefined` if the environment variable is empty, unset, or contains only whitespace. + * + * @param {unknown} value - The value from the config file. + * @returns {string | undefined} - The string value or `undefined`. + */ +export function getStringFromConfigFile(value: unknown): string | undefined { + const raw = String(value)?.trim(); + if (value == null || raw === '') { + return undefined; + } + return raw; +} diff --git a/experimental/packages/opentelemetry-configuration/test/ConfigProvider.test.ts b/experimental/packages/opentelemetry-configuration/test/ConfigProvider.test.ts index 670a09e47e3..7b45b604602 100644 --- a/experimental/packages/opentelemetry-configuration/test/ConfigProvider.test.ts +++ b/experimental/packages/opentelemetry-configuration/test/ConfigProvider.test.ts @@ -20,7 +20,7 @@ import { DiagLogLevel } from '@opentelemetry/api'; import { createConfigProvider } from '../src/ConfigProvider'; const defaultConfig: Configuration = { - disable: false, + disabled: false, log_level: DiagLogLevel.INFO, node_resource_detectors: ['all'], resource: {}, @@ -107,6 +107,144 @@ const defaultConfig: Configuration = { }, }; +const configFromFile: Configuration = { + disabled: false, + log_level: DiagLogLevel.DEBUG, + node_resource_detectors: ['all'], + resource: { + schema_url: 'https://opentelemetry.io/schemas/1.16.0', + attributes_list: 'service.namespace=my-namespace,service.version=1.0.0', + attributes: [ + { + name: 'service.name', + value: 'unknown_service', + type: 'string', + }, + { + name: 'string_key', + value: 'value', + type: 'string', + }, + { + name: 'bool_key', + value: true, + type: 'bool', + }, + { + name: 'int_key', + value: 1, + type: 'int', + }, + { + name: 'double_key', + value: 1.1, + type: 'double', + }, + { + name: 'string_array_key', + value: ['value1', 'value2'], + type: 'string_array', + }, + { + name: 'bool_array_key', + value: [true, false], + type: 'bool_array', + }, + { + name: 'int_array_key', + value: [1, 2], + type: 'int_array', + }, + { + name: 'double_array_key', + value: [1.1, 2.2], + type: 'double_array', + }, + ], + }, + attribute_limits: { + attribute_count_limit: 128, + }, + propagator: { + composite: ['tracecontext', 'baggage'], + composite_list: 'tracecontext,baggage', + }, + tracer_provider: { + processors: [ + { + batch: { + schedule_delay: 5000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/traces', + timeout: 10000, + }, + }, + }, + }, + ], + limits: { + attribute_count_limit: 128, + event_count_limit: 128, + link_count_limit: 128, + event_attribute_count_limit: 128, + link_attribute_count_limit: 128, + }, + sampler: { + parent_based: { + root: 'always_on', + remote_parent_sampled: 'always_on', + remote_parent_not_sampled: 'always_off', + local_parent_sampled: 'always_on', + local_parent_not_sampled: 'always_off', + }, + }, + }, + meter_provider: { + readers: [ + { + periodic: { + interval: 60000, + timeout: 30000, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/metrics', + timeout: 10000, + temporality_preference: 'cumulative', + default_histogram_aggregation: 'explicit_bucket_histogram', + }, + }, + }, + }, + ], + exemplar_filter: 'trace_based', + }, + logger_provider: { + processors: [ + { + batch: { + schedule_delay: 1000, + export_timeout: 30000, + max_queue_size: 2048, + max_export_batch_size: 512, + exporter: { + otlp_http: { + endpoint: 'http://localhost:4318/v1/logs', + timeout: 10000, + }, + }, + }, + }, + ], + limits: { + attribute_count_limit: 128, + }, + }, +}; + describe('ConfigProvider', function () { describe('get values from environment variables', function () { afterEach(function () { @@ -175,7 +313,7 @@ describe('ConfigProvider', function () { process.env.OTEL_SDK_DISABLED = 'true'; const expectedConfig: Configuration = { ...defaultConfig, - disable: true, + disabled: true, }; const configProvider = createConfigProvider(); assert.deepStrictEqual( @@ -463,6 +601,7 @@ describe('ConfigProvider', function () { describe('get values from config file', function () { afterEach(function () { delete process.env.OTEL_EXPERIMENTAL_CONFIG_FILE; + delete process.env.OTEL_NODE_RESOURCE_DETECTORS; }); it('should initialize config with default values from valid config file', function () { @@ -471,7 +610,7 @@ describe('ConfigProvider', function () { const configProvider = createConfigProvider(); assert.deepStrictEqual( configProvider.getInstrumentationConfig(), - defaultConfig + configFromFile ); }); @@ -482,6 +621,13 @@ describe('ConfigProvider', function () { }); }); + it('should return error from invalid config file format', function () { + process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = 'test/fixtures/invalid.yaml'; + assert.throws(() => { + createConfigProvider(); + }); + }); + it('should initialize config with default values with empty string for config file', function () { process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = ''; const configProvider = createConfigProvider(); @@ -499,5 +645,15 @@ describe('ConfigProvider', function () { defaultConfig ); }); + + it('should initialize config with default values from valid short config file', function () { + process.env.OTEL_EXPERIMENTAL_CONFIG_FILE = + 'test/fixtures/short-config.yml'; + const configProvider = createConfigProvider(); + assert.deepStrictEqual( + configProvider.getInstrumentationConfig(), + defaultConfig + ); + }); }); }); diff --git a/experimental/packages/opentelemetry-configuration/test/fixtures/invalid.yaml b/experimental/packages/opentelemetry-configuration/test/fixtures/invalid.yaml new file mode 100644 index 00000000000..3d0a379779d --- /dev/null +++ b/experimental/packages/opentelemetry-configuration/test/fixtures/invalid.yaml @@ -0,0 +1 @@ +file_format: "invalid" \ No newline at end of file diff --git a/experimental/packages/opentelemetry-configuration/test/fixtures/kitchen-sink.yaml b/experimental/packages/opentelemetry-configuration/test/fixtures/kitchen-sink.yaml index a861d98b4a7..85c40c0526e 100644 --- a/experimental/packages/opentelemetry-configuration/test/fixtures/kitchen-sink.yaml +++ b/experimental/packages/opentelemetry-configuration/test/fixtures/kitchen-sink.yaml @@ -14,7 +14,7 @@ file_format: "1.0-rc.1" disabled: false # Configure the log level of the internal logger used by the SDK. # If omitted, info is used. -log_level: info +log_level: debug # Configure general attribute limits. See also tracer_provider.limits, logger_provider.limits. attribute_limits: # Configure max attribute value size. diff --git a/experimental/packages/opentelemetry-configuration/test/fixtures/short-config.yml b/experimental/packages/opentelemetry-configuration/test/fixtures/short-config.yml new file mode 100644 index 00000000000..984c87c34a8 --- /dev/null +++ b/experimental/packages/opentelemetry-configuration/test/fixtures/short-config.yml @@ -0,0 +1,2 @@ +file_format: "1.0-rc.1" +disabled: false \ No newline at end of file diff --git a/experimental/packages/opentelemetry-configuration/test/utils.test.ts b/experimental/packages/opentelemetry-configuration/test/utils.test.ts new file mode 100644 index 00000000000..7adf6d93a9e --- /dev/null +++ b/experimental/packages/opentelemetry-configuration/test/utils.test.ts @@ -0,0 +1,69 @@ +/* + * 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 * as assert from 'assert'; +import * as sinon from 'sinon'; +import { diag } from '@opentelemetry/api'; +import { + getBooleanFromConfigFile, + getNumberFromConfigFile, + getStringFromConfigFile, +} from '../src/utils'; + +describe('config utils', function () { + afterEach(function () { + sinon.restore(); + }); + + it('should return correct values for getBooleanFromConfigFile', function () { + assert.strictEqual(getBooleanFromConfigFile(null), undefined); + assert.strictEqual(getBooleanFromConfigFile(' '), undefined); + assert.strictEqual(getBooleanFromConfigFile(true), true); + assert.strictEqual(getBooleanFromConfigFile('true'), true); + assert.strictEqual(getBooleanFromConfigFile(false), false); + assert.strictEqual(getBooleanFromConfigFile('false'), false); + + const warnStub = sinon.stub(diag, 'warn'); + assert.strictEqual(getBooleanFromConfigFile('non-boolean'), undefined); + sinon.assert.calledOnceWithMatch( + warnStub, + `Unknown value 'non-boolean', expected 'true' or 'false'` + ); + }); + + it('should return correct values for getNumberFromConfigFile', function () { + assert.strictEqual(getNumberFromConfigFile(null), undefined); + assert.strictEqual(getNumberFromConfigFile(' '), undefined); + assert.strictEqual(getNumberFromConfigFile(1), 1); + assert.strictEqual(getNumberFromConfigFile(0), 0); + assert.strictEqual(getNumberFromConfigFile(100), 100); + + const warnStub = sinon.stub(diag, 'warn'); + assert.strictEqual(getNumberFromConfigFile('non-number'), undefined); + sinon.assert.calledOnceWithMatch( + warnStub, + `Unknown value 'non-number', expected a number` + ); + }); + + it('should return correct values for getStringFromConfigFile', function () { + assert.strictEqual(getStringFromConfigFile(null), undefined); + assert.strictEqual(getStringFromConfigFile(' '), undefined); + assert.strictEqual(getStringFromConfigFile(undefined), undefined); + assert.strictEqual(getStringFromConfigFile(1), '1'); + assert.strictEqual(getStringFromConfigFile('string-value'), 'string-value'); + }); +}); diff --git a/package-lock.json b/package-lock.json index cc962f537b0..6a19c089ae3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -826,7 +826,8 @@ "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "2.1.0" + "@opentelemetry/core": "2.1.0", + "yaml": "^1.10.2" }, "devDependencies": { "@opentelemetry/api": "^1.9.0", @@ -26667,7 +26668,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "license": "ISC", "engines": { "node": ">= 6" @@ -30298,7 +30298,8 @@ "nyc": "17.1.0", "sinon": "18.0.1", "ts-loader": "9.5.4", - "typescript": "5.0.4" + "typescript": "5.0.4", + "yaml": "^1.10.2" }, "dependencies": { "@types/node": { @@ -46279,8 +46280,7 @@ "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" }, "yargs": { "version": "17.7.2",