diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index e1ba48d160..04ea9a97b3 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -22,6 +22,30 @@ All notable changes to experimental packages in this project will be documented * fix(sdk-events): remove devDependencies to old `@opentelemetry/api-logs@0.52.0`, `@opentelemetry/api-events@0.52.0` packages [#5013](https://github.com/open-telemetry/opentelemetry-js/pull/5013) @pichlermarc * fix(sdk-logs): remove devDependencies to old `@opentelemetry/api-logs@0.52.0` [#5013](https://github.com/open-telemetry/opentelemetry-js/pull/5013) @pichlermarc * fix(sdk-logs): align LogRecord#setAttribute type with types from `@opentelemetry/api-logs@0.53.0` [#5013](https://github.com/open-telemetry/opentelemetry-js/pull/5013) @pichlermarc +* feat(exporter-*-otlp-*)!: rewrite exporter config logic for testability [#4971](https://github.com/open-telemetry/opentelemetry-js/pull/4971) @pichlermarc + * (user-facing) `getDefaultUrl` was intended for internal use has been removed from all exporters + * (user-facing) `getUrlFromConfig` was intended for internal use and has been removed from all exporters + * (user-facing) `hostname` was intended for internal use and has been removed from all exporters + * (user-facing) `url` was intended for internal use and has been removed from all exporters + * (user-facing) `timeoutMillis` was intended for internal use and has been removed from all exporters + * (user-facing) `onInit` was intended for internal use and has been removed from all exporters +* fix(exporter-*-otlp-*): fixes a bug where signal-specific environment variables would not be applied and the trace-specific one was used instead [#4971](https://github.com/open-telemetry/opentelemetry-js/pull/4971) @pichlermarc + * Fixes: + * `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION` + * `OTEL_EXPORTER_OTLP_LOGS_COMPRESSION` + * `OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE` + * `OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE` + * `OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY` + * `OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY` + * `OTEL_EXPORTER_OTLP_METRICS_INSECURE` + * `OTEL_EXPORTER_OTLP_LOGS_INSECURE` +* feat(otlp-exporter-base)!: do not export functions that are intended for internal use [#4971](https://github.com/open-telemetry/opentelemetry-js/pull/4971) @pichlermarc + * Drops the following functions and types that were intended for internal use from the package exports: + * `parseHeaders` + * `appendResourcePathToUrl` + * `appendResourcePathToUrlIfNeeded` + * `configureExporterTimeout` + * `invalidTimeout` ### :books: (Refine Doc) diff --git a/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts b/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts index 1172126cbd..31f1daf95e 100644 --- a/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts +++ b/experimental/packages/exporter-logs-otlp-grpc/src/OTLPLogExporter.ts @@ -15,22 +15,14 @@ */ import { LogRecordExporter, ReadableLogRecord } from '@opentelemetry/sdk-logs'; -import { baggageUtils, getEnv } from '@opentelemetry/core'; import { OTLPGRPCExporterConfigNode, OTLPGRPCExporterNodeBase, - validateAndNormalizeUrl, - DEFAULT_COLLECTOR_URL, } from '@opentelemetry/otlp-grpc-exporter-base'; import { IExportLogsServiceResponse, ProtobufLogsSerializer, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from './version'; - -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; /** * OTLP Logs Exporter for Node @@ -43,34 +35,12 @@ export class OTLPLogExporter implements LogRecordExporter { constructor(config: OTLPGRPCExporterConfigNode = {}) { - const signalSpecificMetadata = { - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS - ), - }; super( config, - signalSpecificMetadata, + ProtobufLogsSerializer, 'LogsExportService', '/opentelemetry.proto.collector.logs.v1.LogsService/Export', - ProtobufLogsSerializer - ); - } - - getDefaultUrl(config: OTLPGRPCExporterConfigNode) { - return validateAndNormalizeUrl(this.getUrlFromConfig(config)); - } - - getUrlFromConfig(config: OTLPGRPCExporterConfigNode): string { - if (typeof config.url === 'string') { - return config.url; - } - - return ( - getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT || - getEnv().OTEL_EXPORTER_OTLP_ENDPOINT || - DEFAULT_COLLECTOR_URL + 'LOGS' ); } } diff --git a/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts index a0d3c7eb66..996b5180ac 100644 --- a/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-grpc/test/OTLPLogExporter.test.ts @@ -37,7 +37,6 @@ import { IExportLogsServiceRequest, IResourceLogs, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../src/version'; const logsServiceProtoPath = 'opentelemetry/proto/collector/logs/v1/logs_service.proto'; @@ -294,104 +293,9 @@ const testCollectorExporter = (params: TestParams) => { }, 500); }); }); - describe('Logs Exporter with compression', () => { - const envSource = process.env; - it('should return gzip compression algorithm on exporter', () => { - const credentials = useTLS - ? grpc.credentials.createSsl( - fs.readFileSync('./test/certs/ca.crt'), - fs.readFileSync('./test/certs/client.key'), - fs.readFileSync('./test/certs/client.crt') - ) - : grpc.credentials.createInsecure(); - - envSource.OTEL_EXPORTER_OTLP_COMPRESSION = 'gzip'; - collectorExporter = new OTLPLogExporter({ - url: address, - credentials, - metadata: metadata, - }); - assert.strictEqual( - collectorExporter.compression, - CompressionAlgorithm.GZIP - ); - delete envSource.OTEL_EXPORTER_OTLP_COMPRESSION; - }); - }); }); }; -describe('OTLPLogExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new OTLPLogExporter({}); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], 'localhost:4317'); - done(); - }); - }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new OTLPLogExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], 'foo.bar.com'); - done(); - }); - }); -}); - -describe('when configuring via environment', () => { - const envSource = process.env; - - afterEach(function () { - // Ensure we don't pollute other tests if assertions fail - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_HEADERS; - delete envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS; - sinon.restore(); - }); - - it('should use url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual(collectorExporter.url, 'foo.bar'); - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.logs'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual(collectorExporter.url, 'foo.logs'); - }); - it('should include user-agent header by default', () => { - const collectorExporter = new OTLPLogExporter(); - const actualMetadata = - collectorExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('User-Agent'), [ - `OTel-OTLP-Exporter-JavaScript/${VERSION}`, - ]); - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const collectorExporter = new OTLPLogExporter(); - const actualMetadata = - collectorExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['bar']); - }); - it('should not override hard-coded headers config with headers defined via env', () => { - const metadata = new grpc.Metadata(); - metadata.set('foo', 'bar'); - metadata.set('goo', 'lol'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=jar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPLogExporter({ metadata }); - const actualMetadata = - collectorExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['bar']); - assert.deepStrictEqual(actualMetadata.get('goo'), ['lol']); - assert.deepStrictEqual(actualMetadata.get('bar'), ['foo']); - }); -}); - testCollectorExporter({ useTLS: true }); testCollectorExporter({ useTLS: false }); testCollectorExporter({ metadata }); diff --git a/experimental/packages/exporter-logs-otlp-http/src/platform/browser/OTLPLogExporter.ts b/experimental/packages/exporter-logs-otlp-http/src/platform/browser/OTLPLogExporter.ts index 4a9ce917e2..3f4f5c60ff 100644 --- a/experimental/packages/exporter-logs-otlp-http/src/platform/browser/OTLPLogExporter.ts +++ b/experimental/packages/exporter-logs-otlp-http/src/platform/browser/OTLPLogExporter.ts @@ -23,8 +23,6 @@ import type { IExportLogsServiceResponse } from '@opentelemetry/otlp-transformer import { OTLPExporterBrowserBase } from '@opentelemetry/otlp-exporter-base'; import { JsonLogsSerializer } from '@opentelemetry/otlp-transformer'; -import { getDefaultUrl } from '../config'; - /** * Collector Logs Exporter for Web */ @@ -38,11 +36,10 @@ export class OTLPLogExporter ...config, }, JsonLogsSerializer, - 'application/json' + { + 'Content-Type': 'application/json', + }, + 'v1/logs' ); } - - getDefaultUrl(config: OTLPExporterConfigBase): string { - return getDefaultUrl(config); - } } diff --git a/experimental/packages/exporter-logs-otlp-http/src/platform/config.ts b/experimental/packages/exporter-logs-otlp-http/src/platform/config.ts deleted file mode 100644 index 5ce6eb75f7..0000000000 --- a/experimental/packages/exporter-logs-otlp-http/src/platform/config.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 { getEnv } from '@opentelemetry/core'; -import { - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, - OTLPExporterConfigBase, -} from '@opentelemetry/otlp-exporter-base'; - -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/logs'; -export const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; - -/** - * common get default url - * @param config exporter config - * @returns url string - */ -export function getDefaultUrl(config: OTLPExporterConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - const env = getEnv(); - if (env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.length > 0) { - return appendRootPathToUrlIfNeeded(env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT); - } - - if (env.OTEL_EXPORTER_OTLP_ENDPOINT.length > 0) { - return appendResourcePathToUrl( - env.OTEL_EXPORTER_OTLP_ENDPOINT, - DEFAULT_COLLECTOR_RESOURCE_PATH - ); - } - - return DEFAULT_COLLECTOR_URL; -} diff --git a/experimental/packages/exporter-logs-otlp-http/src/platform/node/OTLPLogExporter.ts b/experimental/packages/exporter-logs-otlp-http/src/platform/node/OTLPLogExporter.ts index 1837993e91..93efa8fed9 100644 --- a/experimental/packages/exporter-logs-otlp-http/src/platform/node/OTLPLogExporter.ts +++ b/experimental/packages/exporter-logs-otlp-http/src/platform/node/OTLPLogExporter.ts @@ -20,14 +20,9 @@ import type { } from '@opentelemetry/sdk-logs'; import type { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; import type { IExportLogsServiceResponse } from '@opentelemetry/otlp-transformer'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; -import { - OTLPExporterNodeBase, - parseHeaders, -} from '@opentelemetry/otlp-exporter-base'; +import { OTLPExporterNodeBase } from '@opentelemetry/otlp-exporter-base'; import { JsonLogsSerializer } from '@opentelemetry/otlp-transformer'; -import { getDefaultUrl } from '../config'; import { VERSION } from '../../version'; const USER_AGENT = { @@ -42,25 +37,17 @@ export class OTLPLogExporter implements LogRecordExporter { constructor(config: OTLPExporterNodeConfigBase = {}) { - // load OTEL_EXPORTER_OTLP_LOGS_TIMEOUT env super( { - timeoutMillis: getEnv().OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, ...config, }, JsonLogsSerializer, { - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS - ), - ...parseHeaders(config?.headers), ...USER_AGENT, 'Content-Type': 'application/json', - } + }, + 'LOGS', + 'v1/logs' ); } - - getDefaultUrl(config: OTLPExporterNodeConfigBase): string { - return getDefaultUrl(config); - } } diff --git a/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts index 362a3d65d2..69f93c6ea9 100644 --- a/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-http/test/browser/OTLPLogExporter.test.ts @@ -16,7 +16,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import * as Config from '../../src/platform/config'; import { OTLPLogExporter } from '../../src/platform/browser'; import { OTLPExporterConfigBase } from '@opentelemetry/otlp-exporter-base'; import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; @@ -38,16 +37,6 @@ describe('OTLPLogExporter', () => { }); }); - describe('getDefaultUrl', () => { - it('should call getDefaultUrl', () => { - const getDefaultUrl = sinon.stub(Config, 'getDefaultUrl'); - const exporter = new OTLPLogExporter(); - exporter.getDefaultUrl({}); - // this callCount is 2, because new OTLPLogExporter also call it - assert.strictEqual(getDefaultUrl.callCount, 2); - }); - }); - describe('export - common', () => { let spySend: any; beforeEach(() => { diff --git a/experimental/packages/exporter-logs-otlp-http/test/config.test.ts b/experimental/packages/exporter-logs-otlp-http/test/config.test.ts deleted file mode 100644 index 2f5e8dd1d2..0000000000 --- a/experimental/packages/exporter-logs-otlp-http/test/config.test.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * 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 { getDefaultUrl } from '../src/platform/config'; - -describe('getDefaultUrl', () => { - let envSource: Record; - - if (global.process?.versions?.node === undefined) { - envSource = globalThis as unknown as Record; - } else { - envSource = process.env as Record; - } - - it('should use config url if config url is defined', () => { - const configUrl = 'http://foo.bar/v1/logs/'; - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar.logs/'; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const defaultUrl = getDefaultUrl({ url: configUrl }); - assert.strictEqual(defaultUrl, configUrl); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should use url defined in env that ends with root path and append version and signal path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/logs` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should use url defined in env without checking if path is already present', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/logs'; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should use url defined in env and append version and signal', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.logs/'; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should add root path when signal url defined in env contains no path and no root path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.logs'; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}/` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should not add root path when signal url defined in env contains root path but no path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/'; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should not add root path when signal url defined in env contains path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/v1/logs'; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); - - it('should not add root path when signal url defined in env contains path and ends in /', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/v1/logs/'; - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - const defaultUrl = getDefaultUrl({}); - assert.strictEqual( - defaultUrl, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - }); -}); diff --git a/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts index 8e6b076ed6..726be702fa 100644 --- a/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-http/test/node/OTLPLogExporter.test.ts @@ -18,7 +18,6 @@ import { diag } from '@opentelemetry/api'; import * as assert from 'assert'; import * as http from 'http'; import * as sinon from 'sinon'; -import * as Config from '../../src/platform/config'; import { OTLPLogExporter } from '../../src/platform/node'; import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; @@ -103,15 +102,6 @@ describe('OTLPLogExporter', () => { delete envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS; }); - it('should use timeout defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_LOGS_TIMEOUT = 30000; - const exporter = new OTLPLogExporter(); - assert.strictEqual(exporter.timeoutMillis, 30000); - delete envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS; - delete envSource.OTEL_EXPORTER_OTLP_LOGS_TIMEOUT; - }); - it('should override headers defined via env with headers defined in constructor', () => { envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; const exporter = new OTLPLogExporter({ @@ -131,23 +121,12 @@ describe('OTLPLogExporter', () => { }); }); - describe('getDefaultUrl', () => { - it('should call getDefaultUrl', () => { - const getDefaultUrl = sinon.stub(Config, 'getDefaultUrl'); - const exporter = new OTLPLogExporter(); - exporter.getDefaultUrl({}); - // this callCount is 2, because new OTLPLogExporter also call it - assert.strictEqual(getDefaultUrl.callCount, 2); - }); - }); - describe('export', () => { beforeEach(() => { collectorExporterConfig = { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogExporter.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogExporter.ts index ede7ae6c15..470a40f5fb 100644 --- a/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogExporter.ts +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogExporter.ts @@ -25,9 +25,6 @@ import { import { ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs'; -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/logs'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; - /** * Collector Trace Exporter for Web */ @@ -36,14 +33,11 @@ export class OTLPLogExporter implements LogRecordExporter { constructor(config: OTLPExporterConfigBase = {}) { - super(config, ProtobufLogsSerializer, 'application/x-protobuf'); - } - - getDefaultUrl(config: OTLPExporterConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + ProtobufLogsSerializer, + { 'Content-Type': 'application/x-protobuf' }, + 'v1/logs' + ); } } diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogExporter.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogExporter.ts index 893a06b4ab..828a11cbc6 100644 --- a/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogExporter.ts +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogExporter.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import { getEnv, baggageUtils } from '@opentelemetry/core'; import { OTLPExporterConfigBase, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, OTLPExporterNodeBase, - parseHeaders, } from '@opentelemetry/otlp-exporter-base'; import { IExportLogsServiceResponse, @@ -34,9 +30,6 @@ const USER_AGENT = { 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, }; -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/logs'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; - /** * Collector Trace Exporter for Node */ @@ -45,33 +38,15 @@ export class OTLPLogExporter implements LogRecordExporter { constructor(config: OTLPExporterConfigBase = {}) { - super(config, ProtobufLogsSerializer, { - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS - ), - ...parseHeaders(config?.headers), - ...USER_AGENT, - 'Content-Type': 'application/x-protobuf', - }); - } - - getDefaultUrl(config: OTLPExporterConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - const env = getEnv(); - if (env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.length > 0) { - return appendRootPathToUrlIfNeeded(env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT); - } - - if (env.OTEL_EXPORTER_OTLP_ENDPOINT.length > 0) { - return appendResourcePathToUrl( - env.OTEL_EXPORTER_OTLP_ENDPOINT, - DEFAULT_COLLECTOR_RESOURCE_PATH - ); - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + ProtobufLogsSerializer, + { + ...USER_AGENT, + 'Content-Type': 'application/x-protobuf', + }, + 'LOGS', + 'v1/logs' + ); } } diff --git a/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts index 6a76ed24a0..29ed48ff4a 100644 --- a/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogExporter.test.ts @@ -21,9 +21,7 @@ import { OTLPLogExporter } from '../../src/platform/browser/index'; describe('OTLPLogExporter - web', () => { let collectorLogsExporter: OTLPLogExporter; describe('constructor', () => { - let onInitSpy: any; beforeEach(() => { - onInitSpy = sinon.stub(OTLPLogExporter.prototype, 'onInit'); const collectorExporterConfig = { hostname: 'foo', url: 'http://foo.bar.com', @@ -36,15 +34,5 @@ describe('OTLPLogExporter - web', () => { it('should create an instance', () => { assert.ok(typeof collectorLogsExporter !== 'undefined'); }); - it('should call onInit', () => { - assert.strictEqual(onInitSpy.callCount, 1); - }); - it('should set hostname', () => { - assert.strictEqual(collectorLogsExporter.hostname, 'foo'); - }); - - it('should set url', () => { - assert.strictEqual(collectorLogsExporter.url, 'http://foo.bar.com'); - }); }); }); diff --git a/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts index 9cf3961fe5..475b00ad74 100644 --- a/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts +++ b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogExporter.test.ts @@ -35,7 +35,6 @@ import { } from '@opentelemetry/otlp-exporter-base'; import { IExportLogsServiceRequest } from '@opentelemetry/otlp-transformer'; import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; -import { VERSION } from '../../src/version'; import { Root } from 'protobufjs'; import * as path from 'path'; @@ -67,150 +66,12 @@ describe('OTLPLogExporter - node with proto over http', () => { sinon.restore(); }); - describe('when configuring via environment', () => { - const envSource = process.env; - it('should use url defined in env that ends with root path and append version and signal path', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/logs` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env without checking if path is already present', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/logs'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env and append version and signal', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.logs/'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - }); - it('should override url defined in env with url defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const constructorDefinedEndpoint = 'http://constructor/v1/logs'; - const collectorExporter = new OTLPLogExporter({ - url: constructorDefinedEndpoint, - }); - assert.strictEqual(collectorExporter.url, constructorDefinedEndpoint); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should add root path when signal url defined in env contains no path and no root path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}/` - ); - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains root path but no path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/v1/logs'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path and ends in /', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = 'http://foo.bar/v1/logs/'; - const collectorExporter = new OTLPLogExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; - }); - it('should include user-agent header by default', () => { - const exporter = new OTLPLogExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers'][ - 'User-Agent' - ], - `OTel-OTLP-Exporter-JavaScript/${VERSION}` - ); - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=bar'; - const exporter = new OTLPLogExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'bar' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=boo'; - const exporter = new OTLPLogExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'boo' - ); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override headers defined via env with headers defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - const exporter = new OTLPLogExporter({ - headers: { - foo: 'constructor', - }, - }); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'constructor' - ); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - }); - describe('export', () => { beforeEach(() => { collectorExporterConfig = { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, @@ -346,7 +207,6 @@ describe('OTLPLogExporter - node with proto over http', () => { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, compression: CompressionAlgorithm.GZIP, diff --git a/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts index cffa9b0749..e785475a1d 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-grpc/src/OTLPTraceExporter.ts @@ -15,22 +15,14 @@ */ import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; -import { baggageUtils, getEnv } from '@opentelemetry/core'; import { OTLPGRPCExporterConfigNode, OTLPGRPCExporterNodeBase, - validateAndNormalizeUrl, - DEFAULT_COLLECTOR_URL, } from '@opentelemetry/otlp-grpc-exporter-base'; import { IExportTraceServiceResponse, ProtobufTraceSerializer, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from './version'; - -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; /** * OTLP Trace Exporter for Node @@ -40,34 +32,12 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPGRPCExporterConfigNode = {}) { - const signalSpecificMetadata = { - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ), - }; super( config, - signalSpecificMetadata, + ProtobufTraceSerializer, 'TraceExportService', '/opentelemetry.proto.collector.trace.v1.TraceService/Export', - ProtobufTraceSerializer - ); - } - - getDefaultUrl(config: OTLPGRPCExporterConfigNode) { - return validateAndNormalizeUrl(this.getUrlFromConfig(config)); - } - - getUrlFromConfig(config: OTLPGRPCExporterConfigNode): string { - if (typeof config.url === 'string') { - return config.url; - } - - return ( - getEnv().OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || - getEnv().OTEL_EXPORTER_OTLP_ENDPOINT || - DEFAULT_COLLECTOR_URL + 'TRACES' ); } } diff --git a/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts index 60f250d7f6..801f85a36b 100644 --- a/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-grpc/test/OTLPTraceExporter.test.ts @@ -28,7 +28,6 @@ import * as grpc from '@grpc/grpc-js'; import * as path from 'path'; import * as sinon from 'sinon'; import { OTLPTraceExporter } from '../src'; -import { VERSION } from '../src/version'; import { ensureExportedSpanIsCorrect, @@ -299,104 +298,9 @@ const testCollectorExporter = (params: TestParams) => { }, 500); }); }); - describe('Trace Exporter with compression', () => { - const envSource = process.env; - it('should return gzip compression algorithm on exporter', () => { - const credentials = useTLS - ? grpc.credentials.createSsl( - fs.readFileSync('./test/certs/ca.crt'), - fs.readFileSync('./test/certs/client.key'), - fs.readFileSync('./test/certs/client.crt') - ) - : grpc.credentials.createInsecure(); - - envSource.OTEL_EXPORTER_OTLP_COMPRESSION = 'gzip'; - collectorExporter = new OTLPTraceExporter({ - url: address, - credentials, - metadata: metadata, - }); - assert.strictEqual( - collectorExporter.compression, - CompressionAlgorithm.GZIP - ); - delete envSource.OTEL_EXPORTER_OTLP_COMPRESSION; - }); - }); }); }; -describe('OTLPTraceExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new OTLPTraceExporter({}); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], 'localhost:4317'); - done(); - }); - }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new OTLPTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], 'foo.bar.com'); - done(); - }); - }); -}); - -describe('when configuring via environment', () => { - const envSource = process.env; - - afterEach(function () { - // Ensure we don't pollute other tests if assertions fail - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_HEADERS; - delete envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS; - sinon.restore(); - }); - - it('should use url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual(collectorExporter.url, 'foo.bar'); - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.traces'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual(collectorExporter.url, 'foo.traces'); - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const collectorExporter = new OTLPTraceExporter(); - const actualMetadata = - collectorExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['bar']); - }); - it('should include user agent in header', () => { - const collectorExporter = new OTLPTraceExporter(); - const actualMetadata = - collectorExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('User-Agent'), [ - `OTel-OTLP-Exporter-JavaScript/${VERSION}`, - ]); - }); - it('should not override hard-coded headers config with headers defined via env', () => { - const metadata = new grpc.Metadata(); - metadata.set('foo', 'bar'); - metadata.set('goo', 'lol'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=jar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPTraceExporter({ metadata }); - const actualMetadata = - collectorExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['bar']); - assert.deepStrictEqual(actualMetadata.get('goo'), ['lol']); - assert.deepStrictEqual(actualMetadata.get('bar'), ['foo']); - }); -}); - testCollectorExporter({ useTLS: true }); testCollectorExporter({ useTLS: false }); testCollectorExporter({ metadata }); diff --git a/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts index c9bc8e9242..2e03ef845e 100644 --- a/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-http/src/platform/browser/OTLPTraceExporter.ts @@ -25,7 +25,6 @@ import { } from '@opentelemetry/otlp-transformer'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/traces'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; /** * Collector Trace Exporter for Web @@ -35,14 +34,11 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterConfigBase = {}) { - super(config, JsonTraceSerializer, `application/json`); - } - - getDefaultUrl(config: OTLPExporterConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + JsonTraceSerializer, + { 'Content-Type': 'application/json' }, + DEFAULT_COLLECTOR_RESOURCE_PATH + ); } } diff --git a/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts index aa91b8c237..8d41df8f0d 100644 --- a/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-http/src/platform/node/OTLPTraceExporter.ts @@ -15,22 +15,12 @@ */ import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; -import { - OTLPExporterNodeBase, - parseHeaders, -} from '@opentelemetry/otlp-exporter-base'; -import { - OTLPExporterNodeConfigBase, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, -} from '@opentelemetry/otlp-exporter-base'; +import { OTLPExporterNodeBase } from '@opentelemetry/otlp-exporter-base'; +import { OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; import { IExportTraceServiceResponse } from '@opentelemetry/otlp-transformer'; import { VERSION } from '../../version'; import { JsonTraceSerializer } from '@opentelemetry/otlp-transformer'; -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/traces'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; const USER_AGENT = { 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, }; @@ -43,35 +33,15 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterNodeConfigBase = {}) { - super(config, JsonTraceSerializer, { - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ), - ...parseHeaders(config?.headers), - ...USER_AGENT, - 'Content-Type': 'application/json', - }); - } - - getDefaultUrl(config: OTLPExporterNodeConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - const env = getEnv(); - if (env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT.length > 0) { - return appendRootPathToUrlIfNeeded( - env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT - ); - } - - if (env.OTEL_EXPORTER_OTLP_ENDPOINT.length > 0) { - return appendResourcePathToUrl( - env.OTEL_EXPORTER_OTLP_ENDPOINT, - DEFAULT_COLLECTOR_RESOURCE_PATH - ); - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + JsonTraceSerializer, + { + ...USER_AGENT, + 'Content-Type': 'application/json', + }, + 'TRACES', + 'v1/traces' + ); } } diff --git a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts index 2e234fb2fa..9049caf60d 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/browser/CollectorTraceExporter.test.ts @@ -54,12 +54,8 @@ describe('OTLPTraceExporter - web', () => { }); describe('constructor', () => { - let onInitSpy: any; - beforeEach(() => { - onInitSpy = sinon.stub(OTLPTraceExporter.prototype, 'onInit'); collectorExporterConfig = { - hostname: 'foo', url: 'http://foo.bar.com', }; collectorTraceExporter = new OTLPTraceExporter(collectorExporterConfig); @@ -68,26 +64,11 @@ describe('OTLPTraceExporter - web', () => { it('should create an instance', () => { assert.ok(typeof collectorTraceExporter !== 'undefined'); }); - - it('should call onInit', () => { - assert.strictEqual(onInitSpy.callCount, 1); - }); - - describe('when config contains certain params', () => { - it('should set hostname', () => { - assert.strictEqual(collectorTraceExporter.hostname, 'foo'); - }); - - it('should set url', () => { - assert.strictEqual(collectorTraceExporter.url, 'http://foo.bar.com'); - }); - }); }); describe('export', () => { beforeEach(() => { collectorExporterConfig = { - hostname: 'foo', url: 'http://foo.bar.com', }; }); @@ -202,29 +183,36 @@ describe('OTLPTraceExporter - web', () => { collectorTraceExporter.export(spans, () => {}); queueMicrotask(async () => { - const request = server.requests[0]; - assert.strictEqual(request.method, 'POST'); - assert.strictEqual(request.url, 'http://foo.bar.com'); - - const body = request.requestBody as Blob; - const decoder = new TextDecoder(); - const json = JSON.parse( - decoder.decode(await body.arrayBuffer()) - ) as IExportTraceServiceRequest; - const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; + try { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody as Blob; + const decoder = new TextDecoder(); + const json = JSON.parse( + decoder.decode(await body.arrayBuffer()) + ) as IExportTraceServiceRequest; + const span1 = json.resourceSpans?.[0].scopeSpans?.[0].spans?.[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - ensureSpanIsCorrect(span1); + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + ensureSpanIsCorrect(span1); - const resource = json.resourceSpans?.[0].resource; - assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); - ensureWebResourceIsCorrect(resource); + const resource = json.resourceSpans?.[0].resource; + assert.ok( + typeof resource !== 'undefined', + "resource doesn't exist" + ); + ensureWebResourceIsCorrect(resource); - assert.strictEqual(stubBeacon.callCount, 0); - ensureExportTraceServiceRequestIsSet(json); + assert.strictEqual(stubBeacon.callCount, 0); + ensureExportTraceServiceRequestIsSet(json); - clock.restore(); - done(); + clock.restore(); + done(); + } catch (e) { + done(e); + } }); }); @@ -496,27 +484,6 @@ describe('OTLPTraceExporter - web', () => { }); }); -describe('OTLPTraceExporter - browser (getDefaultUrl)', () => { - it('should default to v1/trace', done => { - const collectorExporter = new OTLPTraceExporter({}); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:4318/v1/traces' - ); - done(); - }); - }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new OTLPTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); - }); - }); -}); - describe('export with retry - real http request destroyed', () => { let server: any; let collectorTraceExporter: OTLPTraceExporter; @@ -563,7 +530,7 @@ describe('export with retry - real http request destroyed', () => { error.message, 'Export failed with retryable status' ); - assert.strictEqual(calls, 6); + assert.strictEqual(calls, 2); done(); } catch (e) { done(e); diff --git a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts index 56f72c08d7..b36be29534 100644 --- a/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-http/test/node/CollectorTraceExporter.test.ts @@ -35,7 +35,6 @@ import { } from '../traceHelper'; import { MockedResponse } from './nodeHelpers'; import { IExportTraceServiceRequest } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../../src/version'; let fakeRequest: PassThrough; @@ -80,144 +79,6 @@ describe('OTLPTraceExporter - node with json over http', () => { }); }); - describe('when configuring via environment', () => { - const envSource = process.env; - it('should use url defined in env that ends with root path and append version and signal path', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/traces` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env without checking if path is already present', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/traces'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env and append version and signal', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.traces/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should override url defined in env with url defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar'; - const constructorDefinedEndpoint = 'http://constructor/v1/traces'; - const collectorExporter = new OTLPTraceExporter({ - url: constructorDefinedEndpoint, - }); - assert.strictEqual(collectorExporter.url, constructorDefinedEndpoint); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should add root path when signal url defined in env contains no path and no root path', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}/` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains root path but no path', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar/v1/traces'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path and ends in /', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = - 'http://foo.bar/v1/traces/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const exporter = new OTLPTraceExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'bar' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should include user agent in header', () => { - const exporter = new OTLPTraceExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers'][ - 'User-Agent' - ], - `OTel-OTLP-Exporter-JavaScript/${VERSION}` - ); - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const exporter = new OTLPTraceExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'boo' - ); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override headers defined via env with headers defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - const exporter = new OTLPTraceExporter({ - headers: { - foo: 'constructor', - }, - }); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'constructor' - ); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - }); - describe('export', () => { beforeEach(() => { stubRequest = sinon.stub(http, 'request').returns(fakeRequest as any); @@ -225,7 +86,6 @@ describe('OTLPTraceExporter - node with json over http', () => { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, @@ -439,7 +299,6 @@ describe('OTLPTraceExporter - node with json over http', () => { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, compression: CompressionAlgorithm.GZIP, @@ -481,27 +340,6 @@ describe('OTLPTraceExporter - node with json over http', () => { }); }); - describe('OTLPTraceExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new OTLPTraceExporter(); - setTimeout(() => { - assert.strictEqual( - collectorExporter['url'], - 'http://localhost:4318/v1/traces' - ); - done(); - }); - }); - - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new OTLPTraceExporter({ url }); - setTimeout(() => { - assert.strictEqual(collectorExporter['url'], url); - done(); - }); - }); - }); describe('export - with timeout', () => { beforeEach(() => { fakeRequest = new Stream.PassThrough(); @@ -516,7 +354,6 @@ describe('OTLPTraceExporter - node with json over http', () => { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, diff --git a/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts index 82d4bc389d..85d208b741 100644 --- a/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-proto/src/platform/browser/OTLPTraceExporter.ts @@ -25,7 +25,6 @@ import { } from '@opentelemetry/otlp-transformer'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/traces'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; /** * Collector Trace Exporter for Web @@ -35,14 +34,11 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterConfigBase = {}) { - super(config, ProtobufTraceSerializer, 'application/x-protobuf'); - } - - getDefaultUrl(config: OTLPExporterConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + ProtobufTraceSerializer, + { 'Content-Type': 'application/x-protobuf' }, + DEFAULT_COLLECTOR_RESOURCE_PATH + ); } } diff --git a/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts b/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts index 79da4ddc28..1e1b35a230 100644 --- a/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts +++ b/experimental/packages/exporter-trace-otlp-proto/src/platform/node/OTLPTraceExporter.ts @@ -15,13 +15,9 @@ */ import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; import { OTLPExporterNodeConfigBase, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, OTLPExporterNodeBase, - parseHeaders, } from '@opentelemetry/otlp-exporter-base'; import { IExportTraceServiceResponse, @@ -29,8 +25,6 @@ import { } from '@opentelemetry/otlp-transformer'; import { VERSION } from '../../version'; -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/traces'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; const USER_AGENT = { 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, }; @@ -43,35 +37,15 @@ export class OTLPTraceExporter implements SpanExporter { constructor(config: OTLPExporterNodeConfigBase = {}) { - super(config, ProtobufTraceSerializer, { - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_TRACES_HEADERS - ), - ...parseHeaders(config?.headers), - ...USER_AGENT, - 'Content-Type': 'application/x-protobuf', - }); - } - - getDefaultUrl(config: OTLPExporterNodeConfigBase) { - if (typeof config.url === 'string') { - return config.url; - } - - const env = getEnv(); - if (env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT.length > 0) { - return appendRootPathToUrlIfNeeded( - env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT - ); - } - - if (env.OTEL_EXPORTER_OTLP_ENDPOINT.length > 0) { - return appendResourcePathToUrl( - env.OTEL_EXPORTER_OTLP_ENDPOINT, - DEFAULT_COLLECTOR_RESOURCE_PATH - ); - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + ProtobufTraceSerializer, + { + ...USER_AGENT, + 'Content-Type': 'application/x-protobuf', + }, + 'TRACES', + 'v1/traces' + ); } } diff --git a/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts index e8187e7296..264d0051f5 100644 --- a/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-proto/test/browser/CollectorTraceExporter.test.ts @@ -21,9 +21,7 @@ import { OTLPTraceExporter } from '../../src/platform/browser/index'; describe('OTLPTraceExporter - web', () => { let collectorTraceExporter: OTLPTraceExporter; describe('constructor', () => { - let onInitSpy: any; beforeEach(() => { - onInitSpy = sinon.stub(OTLPTraceExporter.prototype, 'onInit'); const collectorExporterConfig = { hostname: 'foo', url: 'http://foo.bar.com', @@ -36,15 +34,5 @@ describe('OTLPTraceExporter - web', () => { it('should create an instance', () => { assert.ok(typeof collectorTraceExporter !== 'undefined'); }); - it('should call onInit', () => { - assert.strictEqual(onInitSpy.callCount, 1); - }); - it('should set hostname', () => { - assert.strictEqual(collectorTraceExporter.hostname, 'foo'); - }); - - it('should set url', () => { - assert.strictEqual(collectorTraceExporter.url, 'http://foo.bar.com'); - }); }); }); diff --git a/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts b/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts index 6adf335c1a..b034778e33 100644 --- a/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts +++ b/experimental/packages/exporter-trace-otlp-proto/test/node/OTLPTraceExporter.test.ts @@ -81,142 +81,12 @@ describe('OTLPTraceExporter - node with proto over http', () => { }); }); - describe('when configuring via environment', () => { - const envSource = process.env; - it('should use url defined in env that ends with root path and append version and signal path', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/traces` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env without checking if path is already present', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/traces'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env and append version and signal', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.traces/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should override url defined in env with url defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const constructorDefinedEndpoint = 'http://constructor/v1/traces'; - const collectorExporter = new OTLPTraceExporter({ - url: constructorDefinedEndpoint, - }); - assert.strictEqual(collectorExporter.url, constructorDefinedEndpoint); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should add root path when signal url defined in env contains no path and no root path', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}/` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains root path but no path', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://foo.bar/v1/traces'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path and ends in /', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = - 'http://foo.bar/v1/traces/'; - const collectorExporter = new OTLPTraceExporter(); - assert.strictEqual( - collectorExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = ''; - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const exporter = new OTLPTraceExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'bar' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = 'foo=boo'; - const exporter = new OTLPTraceExporter(); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'boo' - ); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_TRACES_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override headers defined via env with headers defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - const exporter = new OTLPTraceExporter({ - headers: { - foo: 'constructor', - }, - }); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['foo'], - 'constructor' - ); - assert.strictEqual( - exporter['_transport']['_transport']['_parameters']['headers']['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - }); - describe('export', () => { beforeEach(() => { collectorExporterConfig = { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, @@ -366,7 +236,6 @@ describe('OTLPTraceExporter - node with proto over http', () => { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, compression: CompressionAlgorithm.GZIP, diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts index d7d3c1eea3..2602911aa1 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/src/OTLPMetricExporter.ts @@ -22,55 +22,23 @@ import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; import { OTLPGRPCExporterConfigNode, OTLPGRPCExporterNodeBase, - validateAndNormalizeUrl, - DEFAULT_COLLECTOR_URL, } from '@opentelemetry/otlp-grpc-exporter-base'; -import { baggageUtils, getEnv } from '@opentelemetry/core'; import { IExportMetricsServiceResponse, ProtobufMetricsSerializer, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from './version'; -import { parseHeaders } from '@opentelemetry/otlp-exporter-base'; - -const USER_AGENT = { - 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, -}; class OTLPMetricExporterProxy extends OTLPGRPCExporterNodeBase< ResourceMetrics, IExportMetricsServiceResponse > { constructor(config?: OTLPGRPCExporterConfigNode & OTLPMetricExporterOptions) { - const signalSpecificMetadata = { - ...USER_AGENT, - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ), - ...parseHeaders(config?.headers), - }; super( config, - signalSpecificMetadata, + ProtobufMetricsSerializer, 'MetricsExportService', '/opentelemetry.proto.collector.metrics.v1.MetricsService/Export', - ProtobufMetricsSerializer - ); - } - - getDefaultUrl(config: OTLPGRPCExporterConfigNode): string { - return validateAndNormalizeUrl(this.getUrlFromConfig(config)); - } - - getUrlFromConfig(config: OTLPGRPCExporterConfigNode): string { - if (typeof config.url === 'string') { - return config.url; - } - - return ( - getEnv().OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || - getEnv().OTEL_EXPORTER_OTLP_ENDPOINT || - DEFAULT_COLLECTOR_URL + 'METRICS' ); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts index 9ffd1d0d95..833fa2ba7d 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-grpc/test/OTLPMetricExporter.test.ts @@ -41,7 +41,6 @@ import { IExportMetricsServiceRequest, IResourceMetrics, } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../src/version'; import { AggregationTemporalityPreference } from '@opentelemetry/exporter-metrics-otlp-http'; const metricsServiceProtoPath = @@ -302,112 +301,6 @@ const testOTLPMetricExporter = (params: TestParams) => { }); }; -describe('OTLPMetricExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new OTLPMetricExporter(); - setTimeout(() => { - assert.strictEqual(collectorExporter._otlpExporter.url, 'localhost:4317'); - done(); - }); - }); - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new OTLPMetricExporter({ - url, - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }); - setTimeout(() => { - assert.strictEqual(collectorExporter._otlpExporter.url, 'foo.bar.com'); - done(); - }); - }); -}); - -describe('when configuring via environment', () => { - afterEach(function () { - // Ensure we don't pollute other tests if assertions fail - delete envSource.OTEL_EXPORTER_OTLP_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT; - delete envSource.OTEL_EXPORTER_OTLP_HEADERS; - delete envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS; - sinon.restore(); - }); - - const envSource = process.env; - it('should use url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual(collectorExporter._otlpExporter.url, 'foo.bar'); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.metrics'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual(collectorExporter._otlpExporter.url, 'foo.metrics'); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should use override url defined in env with url defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/metrics'; - const constructorDefinedEndpoint = 'http://constructor/v1/metrics'; - const collectorExporter = new OTLPMetricExporter({ - url: constructorDefinedEndpoint, - }); - assert.strictEqual(collectorExporter._otlpExporter.url, 'constructor'); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const collectorExporter = new OTLPMetricExporter(); - const actualMetadata = - collectorExporter._otlpExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['bar']); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should include user agent in header', () => { - const collectorExporter = new OTLPMetricExporter(); - const actualMetadata = - collectorExporter._otlpExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('User-Agent'), [ - `OTel-OTLP-Exporter-JavaScript/${VERSION}`, - ]); - }); - it('should not override hard-coded headers config with headers defined via env', () => { - const metadata = new grpc.Metadata(); - metadata.set('foo', 'bar'); - metadata.set('goo', 'lol'); - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=jar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; - const collectorExporter = new OTLPMetricExporter({ - metadata, - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }); - const actualMetadata = - collectorExporter._otlpExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['bar']); - assert.deepStrictEqual(actualMetadata.get('bar'), ['foo']); - assert.deepStrictEqual(actualMetadata.get('goo'), ['lol']); - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - - it('should override headers defined via env with headers defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - const collectorExporter = new OTLPMetricExporter({ - headers: { - foo: 'constructor', - }, - }); - - const actualMetadata = - collectorExporter._otlpExporter['_transport']['_parameters'].metadata(); - assert.deepStrictEqual(actualMetadata.get('foo'), ['constructor']); - assert.deepStrictEqual(actualMetadata.get('bar'), ['foo']); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); -}); - testOTLPMetricExporter({ useTLS: true }); testOTLPMetricExporter({ useTLS: false }); testOTLPMetricExporter({ metadata }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts index b84c194f91..828cd61b0c 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/browser/OTLPMetricExporter.ts @@ -27,22 +27,18 @@ import { } from '@opentelemetry/otlp-transformer'; const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/metrics'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; class OTLPExporterBrowserProxy extends OTLPExporterBrowserBase< ResourceMetrics, IExportMetricsServiceResponse > { constructor(config?: OTLPMetricExporterOptions & OTLPExporterConfigBase) { - super(config, JsonMetricsSerializer, 'application/json'); - } - - getDefaultUrl(config: OTLPExporterConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + JsonMetricsSerializer, + { 'Content-Type': 'application/json' }, + DEFAULT_COLLECTOR_RESOURCE_PATH + ); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts index bf57b807dc..368858190a 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/src/platform/node/OTLPMetricExporter.ts @@ -15,15 +15,11 @@ */ import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; import { OTLPMetricExporterOptions } from '../../OTLPMetricExporterOptions'; import { OTLPMetricExporterBase } from '../../OTLPMetricExporterBase'; import { OTLPExporterNodeBase, OTLPExporterNodeConfigBase, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, - parseHeaders, } from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceResponse, @@ -31,8 +27,6 @@ import { } from '@opentelemetry/otlp-transformer'; import { VERSION } from '../../version'; -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/metrics'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; const USER_AGENT = { 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, }; @@ -42,36 +36,16 @@ class OTLPExporterNodeProxy extends OTLPExporterNodeBase< IExportMetricsServiceResponse > { constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { - super(config, JsonMetricsSerializer, { - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ), - ...parseHeaders(config?.headers), - ...USER_AGENT, - 'Content-Type': 'application/json', - }); - } - - getDefaultUrl(config: OTLPExporterNodeConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - const env = getEnv(); - if (env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT.length > 0) { - return appendRootPathToUrlIfNeeded( - env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT - ); - } - - if (env.OTEL_EXPORTER_OTLP_ENDPOINT.length > 0) { - return appendResourcePathToUrl( - env.OTEL_EXPORTER_OTLP_ENDPOINT, - DEFAULT_COLLECTOR_RESOURCE_PATH - ); - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + JsonMetricsSerializer, + { + ...USER_AGENT, + 'Content-Type': 'application/json', + }, + 'METRICS', + 'v1/metrics' + ); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts index e4ae9f3926..e6e646f06a 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/common/CollectorMetricExporter.test.ts @@ -36,8 +36,6 @@ class OTLPMetricExporter extends OTLPExporterBase< CollectorExporterConfig, ResourceMetrics > { - onInit() {} - onShutdown() {} send() {} @@ -62,12 +60,8 @@ describe('OTLPMetricExporter - common', () => { }); describe('constructor', () => { - let onInitSpy: any; - beforeEach(async () => { - onInitSpy = sinon.stub(OTLPMetricExporter.prototype, 'onInit'); collectorExporterConfig = { - hostname: 'foo', url: 'http://foo.bar.com', }; collectorExporter = new OTLPMetricExporter(collectorExporterConfig); @@ -87,20 +81,6 @@ describe('OTLPMetricExporter - common', () => { assert.ok(typeof collectorExporter !== 'undefined'); }); - it('should call onInit', () => { - assert.strictEqual(onInitSpy.callCount, 1); - }); - - describe('when config contains certain params', () => { - it('should set hostname', () => { - assert.strictEqual(collectorExporter.hostname, 'foo'); - }); - - it('should set url', () => { - assert.strictEqual(collectorExporter.url, 'http://foo.bar.com'); - }); - }); - describe('when config is missing certain params', () => { beforeEach(() => { collectorExporter = new OTLPMetricExporter(); @@ -180,7 +160,6 @@ describe('OTLPMetricExporter - common', () => { beforeEach(() => { onShutdownSpy = sinon.stub(OTLPMetricExporter.prototype, 'onShutdown'); collectorExporterConfig = { - hostname: 'foo', url: 'http://foo.bar.com', }; collectorExporter = new OTLPMetricExporter(collectorExporterConfig); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts index 7b19b84f02..4b11c946cc 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-http/test/node/CollectorMetricExporter.test.ts @@ -21,9 +21,6 @@ import * as http from 'http'; import * as sinon from 'sinon'; import { AggregationTemporalityPreference, - CumulativeTemporalitySelector, - DeltaTemporalitySelector, - LowMemoryTemporalitySelector, OTLPMetricExporterOptions, } from '../../src'; @@ -55,7 +52,6 @@ import { OTLPExporterNodeConfigBase, } from '@opentelemetry/otlp-exporter-base'; import { IExportMetricsServiceRequest } from '@opentelemetry/otlp-transformer'; -import { VERSION } from '../../src/version'; let fakeRequest: PassThrough; @@ -248,219 +244,6 @@ describe('OTLPMetricExporter - node with json over http', () => { }); }); - describe('when configuring via environment', () => { - const envSource = process.env; - it('should use url defined in env that ends with root path and append version and signal path', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/metrics` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env without checking if path is already present', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/metrics'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env and append version and signal', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.metrics/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should add root path when signal url defined in env contains no path and no root path', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}/` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains root path but no path', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = - 'http://foo.bar/v1/metrics'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path and ends in /', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = - 'http://foo.bar/v1/metrics/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should use override url defined in env with url defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/metrics'; - const constructorDefinedEndpoint = 'http://constructor/v1/metrics'; - const collectorExporter = new OTLPMetricExporter({ - url: constructorDefinedEndpoint, - }); - assert.strictEqual( - collectorExporter._otlpExporter.url, - constructorDefinedEndpoint - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['foo'], - 'bar' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should include user agent in header', () => { - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['User-Agent'], - `OTel-OTLP-Exporter-JavaScript/${VERSION}` - ); - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['foo'], - 'boo' - ); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override headers defined via env with headers defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - const exporter = new OTLPMetricExporter({ - headers: { - foo: 'constructor', - }, - }); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['foo'], - 'constructor' - ); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should use delta temporality defined via env', () => { - for (const envValue of ['delta', 'DELTA', 'DeLTa', 'delta ']) { - envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter['_aggregationTemporalitySelector'], - DeltaTemporalitySelector - ); - } - }); - it('should use cumulative temporality defined via env', () => { - for (const envValue of [ - 'cumulative', - 'CUMULATIVE', - 'CuMULaTIvE', - 'cumulative ', - ]) { - envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter['_aggregationTemporalitySelector'], - CumulativeTemporalitySelector - ); - } - }); - it('should use low memory temporality defined via env', () => { - for (const envValue of [ - 'lowmemory', - 'LOWMEMORY', - 'LoWMeMOrY', - 'lowmemory ', - ]) { - envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter['_aggregationTemporalitySelector'], - LowMemoryTemporalitySelector - ); - } - }); - it('should configure cumulative temporality with invalid value in env', () => { - for (const envValue of ['invalid', ' ']) { - envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = envValue; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter['_aggregationTemporalitySelector'], - CumulativeTemporalitySelector - ); - } - }); - it('should respect explicit config over environment variable', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE = - 'cumulative'; - const exporter = new OTLPMetricExporter({ - temporalityPreference: AggregationTemporalityPreference.DELTA, - }); - assert.strictEqual( - exporter['_aggregationTemporalitySelector'], - DeltaTemporalitySelector - ); - }); - }); - describe('export', () => { beforeEach(async () => { stubRequest = sinon.stub(http, 'request').returns(fakeRequest as any); @@ -468,7 +251,6 @@ describe('OTLPMetricExporter - node with json over http', () => { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, @@ -677,28 +459,4 @@ describe('OTLPMetricExporter - node with json over http', () => { }); }); }); - describe('OTLPMetricExporter - node (getDefaultUrl)', () => { - it('should default to localhost', done => { - const collectorExporter = new OTLPMetricExporter(); - setTimeout(() => { - assert.strictEqual( - collectorExporter._otlpExporter.url, - 'http://localhost:4318/v1/metrics' - ); - done(); - }); - }); - - it('should keep the URL if included', done => { - const url = 'http://foo.bar.com'; - const collectorExporter = new OTLPMetricExporter({ - url, - temporalityPreference: AggregationTemporalityPreference.CUMULATIVE, - }); - setTimeout(() => { - assert.strictEqual(collectorExporter._otlpExporter.url, url); - done(); - }); - }); - }); }); diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts index 4834c5a698..9f594055a5 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/src/OTLPMetricExporter.ts @@ -15,14 +15,10 @@ */ import { OTLPMetricExporterOptions } from '@opentelemetry/exporter-metrics-otlp-http'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; import { ResourceMetrics } from '@opentelemetry/sdk-metrics'; import { OTLPMetricExporterBase } from '@opentelemetry/exporter-metrics-otlp-http'; import { OTLPExporterNodeConfigBase, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, - parseHeaders, OTLPExporterNodeBase, } from '@opentelemetry/otlp-exporter-base'; import { @@ -31,8 +27,6 @@ import { } from '@opentelemetry/otlp-transformer'; import { VERSION } from './version'; -const DEFAULT_COLLECTOR_RESOURCE_PATH = 'v1/metrics'; -const DEFAULT_COLLECTOR_URL = `http://localhost:4318/${DEFAULT_COLLECTOR_RESOURCE_PATH}`; const USER_AGENT = { 'User-Agent': `OTel-OTLP-Exporter-JavaScript/${VERSION}`, }; @@ -42,36 +36,16 @@ class OTLPMetricExporterNodeProxy extends OTLPExporterNodeBase< IExportMetricsServiceResponse > { constructor(config?: OTLPExporterNodeConfigBase & OTLPMetricExporterOptions) { - super(config, ProtobufMetricsSerializer, { - ...baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_METRICS_HEADERS - ), - ...parseHeaders(config?.headers), - ...USER_AGENT, - 'Content-Type': 'application/x-protobuf', - }); - } - - getDefaultUrl(config: OTLPExporterNodeConfigBase): string { - if (typeof config.url === 'string') { - return config.url; - } - - const env = getEnv(); - if (env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT.length > 0) { - return appendRootPathToUrlIfNeeded( - env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT - ); - } - - if (env.OTEL_EXPORTER_OTLP_ENDPOINT.length > 0) { - return appendResourcePathToUrl( - env.OTEL_EXPORTER_OTLP_ENDPOINT, - DEFAULT_COLLECTOR_RESOURCE_PATH - ); - } - - return DEFAULT_COLLECTOR_URL; + super( + config, + ProtobufMetricsSerializer, + { + ...USER_AGENT, + 'Content-Type': 'application/x-protobuf', + }, + 'METRICS', + 'v1/metrics' + ); } } diff --git a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts index 300fd8eb05..42408adab9 100644 --- a/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts +++ b/experimental/packages/opentelemetry-exporter-metrics-otlp-proto/test/OTLPMetricExporter.test.ts @@ -88,156 +88,12 @@ describe('OTLPMetricExporter - node with proto over http', () => { }); }); - describe('when configuring via environment', () => { - const envSource = process.env; - it('should use url defined in env that ends with root path and append version and signal path', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}v1/metrics` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env without checking if path is already present', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/metrics'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use url defined in env and append version and signal', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics` - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should override global exporter url with signal url defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.metrics/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should add root path when signal url defined in env contains no path and no root path', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.bar'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}/` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains root path but no path', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.bar/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = - 'http://foo.bar/v1/metrics'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should not add root path when signal url defined in env contains path and ends in /', () => { - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = - 'http://foo.bar/v1/metrics/'; - const collectorExporter = new OTLPMetricExporter(); - assert.strictEqual( - collectorExporter._otlpExporter.url, - `${envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; - }); - it('should use override url defined in env with url defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/metrics'; - const constructorDefinedEndpoint = 'http://constructor/v1/metrics'; - const collectorExporter = new OTLPMetricExporter({ - url: constructorDefinedEndpoint, - }); - assert.strictEqual( - collectorExporter._otlpExporter.url, - constructorDefinedEndpoint - ); - envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; - }); - it('should use headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar'; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['foo'], - 'bar' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override global headers config with signal headers defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'foo=boo'; - const exporter = new OTLPMetricExporter(); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['foo'], - 'boo' - ); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - it('should override headers defined via env with headers defined in constructor', () => { - envSource.OTEL_EXPORTER_OTLP_HEADERS = 'foo=bar,bar=foo'; - const exporter = new OTLPMetricExporter({ - headers: { - foo: 'constructor', - }, - }); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['foo'], - 'constructor' - ); - assert.strictEqual( - exporter._otlpExporter['_transport']['_transport']['_parameters'][ - 'headers' - ]['bar'], - 'foo' - ); - envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; - }); - }); - describe('export', () => { beforeEach(async () => { collectorExporterConfig = { headers: { foo: 'bar', }, - hostname: 'foo', url: 'http://foo.bar.com', keepAlive: true, httpAgentOptions: { keepAliveMsecs: 2000 }, diff --git a/experimental/packages/otlp-exporter-base/src/OTLPExporterBase.ts b/experimental/packages/otlp-exporter-base/src/OTLPExporterBase.ts index 0e8c37f00d..986447add2 100644 --- a/experimental/packages/otlp-exporter-base/src/OTLPExporterBase.ts +++ b/experimental/packages/otlp-exporter-base/src/OTLPExporterBase.ts @@ -25,7 +25,6 @@ import { OTLPExporterConfigBase, ExportServiceError, } from './types'; -import { configureExporterTimeout } from './util'; /** * Collector Exporter abstract base class @@ -34,12 +33,6 @@ export abstract class OTLPExporterBase< T extends OTLPExporterConfigBase, ExportItem, > { - public readonly url: string; - /** - * @deprecated scheduled for removal. This is only used in tests. - */ - public readonly hostname: string | undefined; - public readonly timeoutMillis: number; protected _concurrencyLimit: number; protected _sendingPromises: Promise[] = []; protected _shutdownOnce: BindOnceFuture; @@ -48,11 +41,6 @@ export abstract class OTLPExporterBase< * @param config */ constructor(config: T = {} as T) { - this.url = this.getDefaultUrl(config); - if (typeof config.hostname === 'string') { - this.hostname = config.hostname; - } - this.shutdown = this.shutdown.bind(this); this._shutdownOnce = new BindOnceFuture(this._shutdown, this); @@ -60,11 +48,6 @@ export abstract class OTLPExporterBase< typeof config.concurrencyLimit === 'number' ? config.concurrencyLimit : 30; - - this.timeoutMillis = configureExporterTimeout(config.timeoutMillis); - - // platform dependent - this.onInit(config); } /** @@ -138,11 +121,9 @@ export abstract class OTLPExporterBase< } abstract onShutdown(): void; - abstract onInit(config: T): void; abstract send( items: ExportItem[], onSuccess: () => void, onError: (error: OTLPExporterError) => void ): void; - abstract getDefaultUrl(config: T): string; } diff --git a/experimental/packages/otlp-exporter-base/src/configuration/otlp-http-configuration.ts b/experimental/packages/otlp-exporter-base/src/configuration/otlp-http-configuration.ts new file mode 100644 index 0000000000..a303bd5c73 --- /dev/null +++ b/experimental/packages/otlp-exporter-base/src/configuration/otlp-http-configuration.ts @@ -0,0 +1,104 @@ +/* + * 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 { + getSharedConfigurationDefaults, + mergeOtlpSharedConfigurationWithDefaults, + OtlpSharedConfiguration, +} from './shared-configuration'; +import { validateAndNormalizeHeaders } from '../util'; + +export interface OtlpHttpConfiguration extends OtlpSharedConfiguration { + url: string; + headers: Record; +} + +function mergeHeaders( + userProvidedHeaders: Record | undefined | null, + fallbackHeaders: Record | undefined | null, + defaultHeaders: Record +): Record { + const requiredHeaders = { + ...defaultHeaders, + }; + const headers = {}; + + // add fallback ones first + if (fallbackHeaders != null) { + Object.assign(headers, fallbackHeaders); + } + + // override with user-provided ones + if (userProvidedHeaders != null) { + Object.assign(headers, userProvidedHeaders); + } + + // override required ones. + return Object.assign(headers, requiredHeaders); +} + +function validateUserProvidedUrl(url: string | undefined): string | undefined { + if (url == null) { + return undefined; + } + try { + new URL(url); + return url; + } catch (e) { + throw new Error( + `Configuration: Could not parse user-provided export URL: '${url}'` + ); + } +} + +/** + * @param userProvidedConfiguration Configuration options provided by the user in code. + * @param fallbackConfiguration Fallback to use when the {@link userProvidedConfiguration} does not specify an option. + * @param defaultConfiguration The defaults as defined by the exporter specification + */ +export function mergeOtlpHttpConfigurationWithDefaults( + userProvidedConfiguration: Partial, + fallbackConfiguration: Partial, + defaultConfiguration: OtlpHttpConfiguration +): OtlpHttpConfiguration { + return { + ...mergeOtlpSharedConfigurationWithDefaults( + userProvidedConfiguration, + fallbackConfiguration, + defaultConfiguration + ), + headers: mergeHeaders( + validateAndNormalizeHeaders(userProvidedConfiguration.headers), + fallbackConfiguration.headers, + defaultConfiguration.headers + ), + url: + validateUserProvidedUrl(userProvidedConfiguration.url) ?? + fallbackConfiguration.url ?? + defaultConfiguration.url, + }; +} + +export function getHttpConfigurationDefaults( + requiredHeaders: Record, + signalResourcePath: string +): OtlpHttpConfiguration { + return { + ...getSharedConfigurationDefaults(), + headers: requiredHeaders, + url: 'http://localhost:4318/' + signalResourcePath, + }; +} diff --git a/experimental/packages/otlp-exporter-base/src/configuration/otlp-http-env-configuration.ts b/experimental/packages/otlp-exporter-base/src/configuration/otlp-http-env-configuration.ts new file mode 100644 index 0000000000..af15de4c85 --- /dev/null +++ b/experimental/packages/otlp-exporter-base/src/configuration/otlp-http-env-configuration.ts @@ -0,0 +1,131 @@ +/* + * 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 { baggageUtils } from '@opentelemetry/core'; +import { diag } from '@opentelemetry/api'; +import { getSharedConfigurationFromEnvironment } from './shared-env-configuration'; +import { OtlpHttpConfiguration } from './otlp-http-configuration'; + +function getHeadersFromEnv(signalIdentifier: string) { + const signalSpecificRawHeaders = + process.env[`OTEL_EXPORTER_OTLP_${signalIdentifier}_HEADERS`]?.trim(); + const nonSignalSpecificRawHeaders = + process.env['OTEL_EXPORTER_OTLP_HEADERS']?.trim(); + + const signalSpecificHeaders = baggageUtils.parseKeyPairsIntoRecord( + signalSpecificRawHeaders + ); + const nonSignalSpecificHeaders = baggageUtils.parseKeyPairsIntoRecord( + nonSignalSpecificRawHeaders + ); + + if ( + Object.keys(signalSpecificHeaders).length === 0 && + Object.keys(nonSignalSpecificHeaders).length === 0 + ) { + return undefined; + } + + // headers are combined instead of overwritten, with the specific headers taking precedence over + // the non-specific ones. + return Object.assign( + {}, + baggageUtils.parseKeyPairsIntoRecord(nonSignalSpecificRawHeaders), + baggageUtils.parseKeyPairsIntoRecord(signalSpecificRawHeaders) + ); +} + +function appendRootPathToUrlIfNeeded(url: string): string | undefined { + try { + const parsedUrl = new URL(url); + // This will automatically append '/' if there's no root path. + return parsedUrl.toString(); + } catch { + diag.warn( + `Configuration: Could not parse environment-provided export URL: '${url}', falling back to undefined` + ); + return undefined; + } +} + +function appendResourcePathToUrl( + url: string, + path: string +): string | undefined { + try { + // just try to parse, if it fails we catch and warn. + new URL(url); + } catch { + diag.warn( + `Configuration: Could not parse environment-provided export URL: '${url}', falling back to undefined` + ); + return undefined; + } + + if (!url.endsWith('/')) { + url = url + '/'; + } + url += path; + + try { + // just try to parse, if it fails we catch and warn. + new URL(url); + } catch { + diag.warn( + `Configuration: Provided URL appended with '${path}' is not a valid URL, using 'undefined' instead of '${url}'` + ); + return undefined; + } + + return url; +} + +function getNonSpecificUrlFromEnv( + signalResourcePath: string +): string | undefined { + const envUrl = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim(); + if (envUrl == null || envUrl === '') { + return undefined; + } + return appendResourcePathToUrl(envUrl, signalResourcePath); +} + +function getSpecificUrlFromEnv(signalIdentifier: string): string | undefined { + const envUrl = + process.env[`OTEL_EXPORTER_OTLP_${signalIdentifier}_ENDPOINT`]?.trim(); + if (envUrl == null || envUrl === '') { + return undefined; + } + return appendRootPathToUrlIfNeeded(envUrl); +} + +/** + * Reads and returns configuration from the environment + * + * @param signalIdentifier all caps part in environment variables that identifies the signal (e.g.: METRICS, TRACES, LOGS) + * @param signalResourcePath signal resource path to append if necessary (e.g.: v1/metrics, v1/traces, v1/logs) + */ +export function getHttpConfigurationFromEnvironment( + signalIdentifier: string, + signalResourcePath: string +): Partial { + return { + ...getSharedConfigurationFromEnvironment(signalIdentifier), + url: + getSpecificUrlFromEnv(signalIdentifier) ?? + getNonSpecificUrlFromEnv(signalResourcePath), + headers: getHeadersFromEnv(signalIdentifier), + }; +} diff --git a/experimental/packages/otlp-exporter-base/src/configuration/shared-configuration.ts b/experimental/packages/otlp-exporter-base/src/configuration/shared-configuration.ts new file mode 100644 index 0000000000..e69fe35bee --- /dev/null +++ b/experimental/packages/otlp-exporter-base/src/configuration/shared-configuration.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +/** + * Configuration shared across all OTLP exporters + * + * Implementation note: anything added here MUST be + * - platform-agnostic + * - signal-agnostic + * - transport-agnostic + */ +export interface OtlpSharedConfiguration { + timeoutMillis: number; + concurrencyLimit: number; + compression: 'gzip' | 'none'; +} + +export function validateTimeoutMillis(timeoutMillis: number) { + if ( + !Number.isNaN(timeoutMillis) && + Number.isFinite(timeoutMillis) && + timeoutMillis > 0 + ) { + return timeoutMillis; + } + throw new Error( + `Configuration: timeoutMillis is invalid, expected number greater than 0 (actual: '${timeoutMillis}')` + ); +} + +/** + * @param userProvidedConfiguration Configuration options provided by the user in code. + * @param fallbackConfiguration Fallback to use when the {@link userProvidedConfiguration} does not specify an option. + * @param defaultConfiguration The defaults as defined by the exporter specification + */ +export function mergeOtlpSharedConfigurationWithDefaults( + userProvidedConfiguration: Partial, + fallbackConfiguration: Partial, + defaultConfiguration: OtlpSharedConfiguration +): OtlpSharedConfiguration { + return { + timeoutMillis: validateTimeoutMillis( + userProvidedConfiguration.timeoutMillis ?? + fallbackConfiguration.timeoutMillis ?? + defaultConfiguration.timeoutMillis + ), + concurrencyLimit: + userProvidedConfiguration.concurrencyLimit ?? + fallbackConfiguration.concurrencyLimit ?? + defaultConfiguration.concurrencyLimit, + compression: + userProvidedConfiguration.compression ?? + fallbackConfiguration.compression ?? + defaultConfiguration.compression, + }; +} + +export function getSharedConfigurationDefaults(): OtlpSharedConfiguration { + return { + timeoutMillis: 10000, + concurrencyLimit: 30, + compression: 'none', + }; +} diff --git a/experimental/packages/otlp-exporter-base/src/configuration/shared-env-configuration.ts b/experimental/packages/otlp-exporter-base/src/configuration/shared-env-configuration.ts new file mode 100644 index 0000000000..767ec05f4a --- /dev/null +++ b/experimental/packages/otlp-exporter-base/src/configuration/shared-env-configuration.ts @@ -0,0 +1,88 @@ +/* + * 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 { OtlpSharedConfiguration } from './shared-configuration'; +import { diag } from '@opentelemetry/api'; + +function parseAndValidateTimeoutFromEnv( + timeoutEnvVar: string +): number | undefined { + const envTimeout = process.env[timeoutEnvVar]?.trim(); + if (envTimeout != null && envTimeout !== '') { + const definedTimeout = Number(envTimeout); + if ( + !Number.isNaN(definedTimeout) && + Number.isFinite(definedTimeout) && + definedTimeout > 0 + ) { + return definedTimeout; + } + diag.warn( + `Configuration: ${timeoutEnvVar} is invalid, expected number greater than 0 (actual: ${envTimeout})` + ); + } + return undefined; +} + +function getTimeoutFromEnv(signalIdentifier: string) { + const specificTimeout = parseAndValidateTimeoutFromEnv( + `OTEL_EXPORTER_OTLP_${signalIdentifier}_TIMEOUT` + ); + const nonSpecificTimeout = parseAndValidateTimeoutFromEnv( + 'OTEL_EXPORTER_OTLP_TIMEOUT' + ); + + return specificTimeout ?? nonSpecificTimeout; +} + +function parseAndValidateCompressionFromEnv( + compressionEnvVar: string +): 'none' | 'gzip' | undefined { + const compression = process.env[compressionEnvVar]?.trim(); + if (compression === '') { + return undefined; + } + + if (compression == null || compression === 'none' || compression === 'gzip') { + return compression; + } + + diag.warn( + `Configuration: ${compressionEnvVar} is invalid, expected 'none' or 'gzip' (actual: '${compression}')` + ); + return undefined; +} + +function getCompressionFromEnv( + signalIdentifier: string +): 'none' | 'gzip' | undefined { + const specificCompression = parseAndValidateCompressionFromEnv( + `OTEL_EXPORTER_OTLP_${signalIdentifier}_COMPRESSION` + ); + const nonSpecificCompression = parseAndValidateCompressionFromEnv( + 'OTEL_EXPORTER_OTLP_COMPRESSION' + ); + + return specificCompression ?? nonSpecificCompression; +} + +export function getSharedConfigurationFromEnvironment( + signalIdentifier: string +): Partial { + return { + timeoutMillis: getTimeoutFromEnv(signalIdentifier), + compression: getCompressionFromEnv(signalIdentifier), + }; +} diff --git a/experimental/packages/otlp-exporter-base/src/index.ts b/experimental/packages/otlp-exporter-base/src/index.ts index 2150f6c2c5..5edc2d680a 100644 --- a/experimental/packages/otlp-exporter-base/src/index.ts +++ b/experimental/packages/otlp-exporter-base/src/index.ts @@ -24,13 +24,7 @@ export { OTLPExporterConfigBase, ExportServiceError, } from './types'; -export { - parseHeaders, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, - configureExporterTimeout, - invalidTimeout, -} from './util'; +export { validateAndNormalizeHeaders } from './util'; export { ExportResponse, @@ -40,3 +34,11 @@ export { } from './export-response'; export { IExporterTransport } from './exporter-transport'; + +export { + OtlpSharedConfiguration, + mergeOtlpSharedConfigurationWithDefaults, + getSharedConfigurationDefaults, +} from './configuration/shared-configuration'; + +export { getSharedConfigurationFromEnvironment } from './configuration/shared-env-configuration'; diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts index 3313c283d7..da5401698c 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/OTLPExporterBrowserBase.ts @@ -16,14 +16,16 @@ import { OTLPExporterBase } from '../../OTLPExporterBase'; import { OTLPExporterConfigBase, OTLPExporterError } from '../../types'; -import { parseHeaders } from '../../util'; import { diag } from '@opentelemetry/api'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; import { ISerializer } from '@opentelemetry/otlp-transformer'; import { IExporterTransport } from '../../exporter-transport'; import { createXhrTransport } from './xhr-transport'; import { createSendBeaconTransport } from './send-beacon-transport'; import { createRetryingTransport } from '../../retrying-transport'; +import { + getHttpConfigurationDefaults, + mergeOtlpHttpConfigurationWithDefaults, +} from '../../configuration/otlp-http-configuration'; /** * Collector Metric Exporter abstract base class @@ -34,46 +36,55 @@ export abstract class OTLPExporterBrowserBase< > extends OTLPExporterBase { private _serializer: ISerializer; private _transport: IExporterTransport; + private _timeoutMillis: number; /** * @param config * @param serializer - * @param contentType + * @param requiredHeaders + * @param signalResourcePath */ constructor( config: OTLPExporterConfigBase = {}, serializer: ISerializer, - contentType: string + requiredHeaders: Record, + signalResourcePath: string ) { super(config); this._serializer = serializer; const useXhr = !!config.headers || typeof navigator.sendBeacon !== 'function'; + + const actualConfig = mergeOtlpHttpConfigurationWithDefaults( + { + url: config.url, + timeoutMillis: config.timeoutMillis, + headers: config.headers, + concurrencyLimit: config.concurrencyLimit, + }, + {}, // no fallback for browser case + getHttpConfigurationDefaults(requiredHeaders, signalResourcePath) + ); + + this._timeoutMillis = actualConfig.timeoutMillis; + this._concurrencyLimit = actualConfig.concurrencyLimit; + if (useXhr) { this._transport = createRetryingTransport({ transport: createXhrTransport({ - headers: Object.assign( - {}, - parseHeaders(config.headers), - baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_HEADERS - ), - { 'Content-Type': contentType } - ), - url: this.url, + headers: actualConfig.headers, + url: actualConfig.url, }), }); } else { // sendBeacon has no way to signal retry, so we do not wrap it in a RetryingTransport this._transport = createSendBeaconTransport({ - url: this.url, - blobType: contentType, + url: actualConfig.url, + blobType: actualConfig.headers['Content-Type'], }); } } - onInit(): void {} - onShutdown(): void {} send( @@ -94,7 +105,7 @@ export abstract class OTLPExporterBrowserBase< } const promise = this._transport - .send(data, this.timeoutMillis) + .send(data, this._timeoutMillis) .then(response => { if (response.status === 'success') { onSuccess(); diff --git a/experimental/packages/otlp-exporter-base/src/platform/browser/xhr-transport.ts b/experimental/packages/otlp-exporter-base/src/platform/browser/xhr-transport.ts index 7e36929a6a..6517a994b6 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/browser/xhr-transport.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/browser/xhr-transport.ts @@ -16,8 +16,11 @@ import { IExporterTransport } from '../../exporter-transport'; import { ExportResponse } from '../../export-response'; -import { isExportRetryable, parseRetryAfterToMills } from '../../util'; import { diag } from '@opentelemetry/api'; +import { + isExportRetryable, + parseRetryAfterToMills, +} from '../../is-export-retryable'; export interface XhrRequestParameters { url: string; diff --git a/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts b/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts index 2c66d8bca9..f5d858016c 100644 --- a/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts +++ b/experimental/packages/otlp-exporter-base/src/platform/node/OTLPExporterNodeBase.ts @@ -16,14 +16,18 @@ import { OTLPExporterBase } from '../../OTLPExporterBase'; import { OTLPExporterNodeConfigBase } from './types'; -import { configureCompression } from './util'; import { diag } from '@opentelemetry/api'; -import { getEnv, baggageUtils } from '@opentelemetry/core'; import { ISerializer } from '@opentelemetry/otlp-transformer'; import { IExporterTransport } from '../../exporter-transport'; import { createHttpExporterTransport } from './http-exporter-transport'; import { OTLPExporterError } from '../../types'; import { createRetryingTransport } from '../../retrying-transport'; +import { convertLegacyAgentOptions } from './convert-legacy-agent-options'; +import { + getHttpConfigurationDefaults, + mergeOtlpHttpConfigurationWithDefaults, +} from '../../configuration/otlp-http-configuration'; +import { getHttpConfigurationFromEnvironment } from '../../configuration/otlp-http-env-configuration'; /** * Collector Metric Exporter abstract base class @@ -34,54 +38,47 @@ export abstract class OTLPExporterNodeBase< > extends OTLPExporterBase { private _serializer: ISerializer; private _transport: IExporterTransport; + private _timeoutMillis: number; constructor( config: OTLPExporterNodeConfigBase = {}, serializer: ISerializer, - signalSpecificHeaders: Record + requiredHeaders: Record, + signalIdentifier: string, + signalResourcePath: string ) { super(config); + const actualConfig = mergeOtlpHttpConfigurationWithDefaults( + { + url: config.url, + headers: config.headers, + concurrencyLimit: config.concurrencyLimit, + timeoutMillis: config.timeoutMillis, + compression: config.compression, + }, + getHttpConfigurationFromEnvironment(signalIdentifier, signalResourcePath), + getHttpConfigurationDefaults(requiredHeaders, signalResourcePath) + ); + + this._timeoutMillis = actualConfig.timeoutMillis; + this._concurrencyLimit = actualConfig.concurrencyLimit; + // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((config as any).metadata) { diag.warn('Metadata cannot be set when using http'); } this._serializer = serializer; - // populate keepAlive for use with new settings - if (config?.keepAlive != null) { - if (config.httpAgentOptions != null) { - if (config.httpAgentOptions.keepAlive == null) { - // specific setting is not set, populate with non-specific setting. - config.httpAgentOptions.keepAlive = config.keepAlive; - } - // do nothing, use specific setting otherwise - } else { - // populate specific option if AgentOptions does not exist. - config.httpAgentOptions = { - keepAlive: config.keepAlive, - }; - } - } - const nonSignalSpecificHeaders = baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_HEADERS - ); - this._transport = createRetryingTransport({ transport: createHttpExporterTransport({ - agentOptions: config.httpAgentOptions ?? { keepAlive: true }, - compression: configureCompression(config.compression), - headers: Object.assign( - {}, - nonSignalSpecificHeaders, - signalSpecificHeaders - ), - url: this.url, + agentOptions: convertLegacyAgentOptions(config), + compression: actualConfig.compression, + headers: actualConfig.headers, + url: actualConfig.url, }), }); } - onInit(_config: OTLPExporterNodeConfigBase): void {} - send( objects: ExportItem[], onSuccess: () => void, @@ -100,7 +97,7 @@ export abstract class OTLPExporterNodeBase< } const promise = this._transport - .send(data, this.timeoutMillis) + .send(data, this._timeoutMillis) .then(response => { if (response.status === 'success') { onSuccess(); diff --git a/experimental/packages/otlp-exporter-base/src/platform/node/convert-legacy-agent-options.ts b/experimental/packages/otlp-exporter-base/src/platform/node/convert-legacy-agent-options.ts new file mode 100644 index 0000000000..a73c5b7ead --- /dev/null +++ b/experimental/packages/otlp-exporter-base/src/platform/node/convert-legacy-agent-options.ts @@ -0,0 +1,46 @@ +/* + * 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 { OTLPExporterNodeConfigBase } from './types'; +import type * as http from 'http'; +import type * as https from 'https'; + +/** + * Replicates old config behavior where there's two ways to set keepAlive. + * This function sets keepAlive in AgentOptions if it is defined. In the future, we will remove + * this duplicate to only allow setting keepAlive in AgentOptions. + * @param config + */ +export function convertLegacyAgentOptions( + config: OTLPExporterNodeConfigBase +): http.AgentOptions | https.AgentOptions { + // populate keepAlive for use with new settings + if (config?.keepAlive != null) { + if (config.httpAgentOptions != null) { + if (config.httpAgentOptions.keepAlive == null) { + // specific setting is not set, populate with non-specific setting. + config.httpAgentOptions.keepAlive = config.keepAlive; + } + // do nothing, use specific setting otherwise + } else { + // populate specific option if AgentOptions does not exist. + config.httpAgentOptions = { + keepAlive: config.keepAlive, + }; + } + } + + return config.httpAgentOptions ?? { keepAlive: true }; +} diff --git a/experimental/packages/otlp-exporter-base/src/platform/node/util.ts b/experimental/packages/otlp-exporter-base/src/platform/node/util.ts deleted file mode 100644 index 68125fb299..0000000000 --- a/experimental/packages/otlp-exporter-base/src/platform/node/util.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { CompressionAlgorithm } from './types'; -import { getEnv } from '@opentelemetry/core'; - -export function configureCompression( - compression: CompressionAlgorithm | undefined -): CompressionAlgorithm { - if (compression) { - return compression; - } else { - const definedCompression = - getEnv().OTEL_EXPORTER_OTLP_TRACES_COMPRESSION || - getEnv().OTEL_EXPORTER_OTLP_COMPRESSION; - return definedCompression === CompressionAlgorithm.GZIP - ? CompressionAlgorithm.GZIP - : CompressionAlgorithm.NONE; - } -} diff --git a/experimental/packages/otlp-exporter-base/src/types.ts b/experimental/packages/otlp-exporter-base/src/types.ts index a8ee1db743..7964f0048c 100644 --- a/experimental/packages/otlp-exporter-base/src/types.ts +++ b/experimental/packages/otlp-exporter-base/src/types.ts @@ -45,8 +45,7 @@ export interface ExportServiceError { * Collector Exporter base config */ export interface OTLPExporterConfigBase { - headers?: Partial>; - hostname?: string; + headers?: Record; url?: string; concurrencyLimit?: number; /** Maximum time the OTLP exporter will wait for each batch export. diff --git a/experimental/packages/otlp-exporter-base/src/util.ts b/experimental/packages/otlp-exporter-base/src/util.ts index 2824752e10..515b913402 100644 --- a/experimental/packages/otlp-exporter-base/src/util.ts +++ b/experimental/packages/otlp-exporter-base/src/util.ts @@ -15,19 +15,12 @@ */ import { diag } from '@opentelemetry/api'; -import { getEnv } from '@opentelemetry/core'; - -const DEFAULT_TRACE_TIMEOUT = 10000; -export const DEFAULT_EXPORT_MAX_ATTEMPTS = 5; -export const DEFAULT_EXPORT_INITIAL_BACKOFF = 1000; -export const DEFAULT_EXPORT_MAX_BACKOFF = 5000; -export const DEFAULT_EXPORT_BACKOFF_MULTIPLIER = 1.5; /** * Parses headers from config leaving only those that have defined values * @param partialHeaders */ -export function parseHeaders( +export function validateAndNormalizeHeaders( partialHeaders: Partial> = {} ): Record { const headers: Record = {}; @@ -42,100 +35,3 @@ export function parseHeaders( }); return headers; } - -/** - * Adds path (version + signal) to a no per-signal endpoint - * @param url - * @param path - * @returns url + path - */ -export function appendResourcePathToUrl(url: string, path: string): string { - if (!url.endsWith('/')) { - url = url + '/'; - } - return url + path; -} - -/** - * Adds root path to signal specific endpoint when endpoint contains no path part and no root path - * @param url - * @returns url - */ -export function appendRootPathToUrlIfNeeded(url: string): string { - try { - const parsedUrl = new URL(url); - if (parsedUrl.pathname === '') { - parsedUrl.pathname = parsedUrl.pathname + '/'; - } - return parsedUrl.toString(); - } catch { - diag.warn(`Could not parse export URL: '${url}'`); - return url; - } -} - -/** - * Configure exporter trace timeout value from passed in value or environment variables - * @param timeoutMillis - * @returns timeout value in milliseconds - */ -export function configureExporterTimeout( - timeoutMillis: number | undefined -): number { - if (typeof timeoutMillis === 'number') { - if (timeoutMillis <= 0) { - // OTLP exporter configured timeout - using default value of 10000ms - return invalidTimeout(timeoutMillis, DEFAULT_TRACE_TIMEOUT); - } - return timeoutMillis; - } else { - return getExporterTimeoutFromEnv(); - } -} - -function getExporterTimeoutFromEnv(): number { - const definedTimeout = Number( - getEnv().OTEL_EXPORTER_OTLP_TRACES_TIMEOUT ?? - getEnv().OTEL_EXPORTER_OTLP_TIMEOUT - ); - - if (definedTimeout <= 0) { - // OTLP exporter configured timeout - using default value of 10000ms - return invalidTimeout(definedTimeout, DEFAULT_TRACE_TIMEOUT); - } else { - return definedTimeout; - } -} - -// OTLP exporter configured timeout - using default value of 10000ms -export function invalidTimeout( - timeout: number, - defaultTimeout: number -): number { - diag.warn('Timeout must be greater than 0', timeout); - - return defaultTimeout; -} - -export function isExportRetryable(statusCode: number): boolean { - const retryCodes = [429, 502, 503, 504]; - - return retryCodes.includes(statusCode); -} - -export function parseRetryAfterToMills(retryAfter?: string | null): number { - if (retryAfter == null) { - return -1; - } - const seconds = Number.parseInt(retryAfter, 10); - if (Number.isInteger(seconds)) { - return seconds > 0 ? seconds * 1000 : -1; - } - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#directives - const delay = new Date(retryAfter).getTime() - Date.now(); - - if (delay >= 0) { - return delay; - } - return 0; -} diff --git a/experimental/packages/otlp-exporter-base/test/common/CollectorExporter.test.ts b/experimental/packages/otlp-exporter-base/test/common/CollectorExporter.test.ts index c6be6965e6..89874f353d 100644 --- a/experimental/packages/otlp-exporter-base/test/common/CollectorExporter.test.ts +++ b/experimental/packages/otlp-exporter-base/test/common/CollectorExporter.test.ts @@ -27,7 +27,6 @@ class OTLPTraceExporter extends OTLPExporterBase< CollectorExporterConfig, ComplexTestObject > { - onInit() {} onShutdown() {} send( items: any[], @@ -55,12 +54,8 @@ describe('OTLPTraceExporter - common', () => { }); describe('constructor', () => { - let onInitSpy: any; - beforeEach(() => { - onInitSpy = sinon.stub(OTLPTraceExporter.prototype, 'onInit'); collectorExporterConfig = { - hostname: 'foo', url: 'http://foo.bar.com', }; collectorExporter = new OTLPTraceExporter(collectorExporterConfig); @@ -69,20 +64,6 @@ describe('OTLPTraceExporter - common', () => { it('should create an instance', () => { assert.ok(typeof collectorExporter !== 'undefined'); }); - - it('should call onInit', () => { - assert.strictEqual(onInitSpy.callCount, 1); - }); - - describe('when config contains certain params', () => { - it('should set hostname', () => { - assert.strictEqual(collectorExporter.hostname, 'foo'); - }); - - it('should set url', () => { - assert.strictEqual(collectorExporter.url, 'http://foo.bar.com'); - }); - }); }); describe('export', () => { @@ -190,7 +171,6 @@ describe('OTLPTraceExporter - common', () => { beforeEach(() => { onShutdownSpy = sinon.stub(OTLPTraceExporter.prototype, 'onShutdown'); collectorExporterConfig = { - hostname: 'foo', url: 'http://foo.bar.com', }; collectorExporter = new OTLPTraceExporter(collectorExporterConfig); diff --git a/experimental/packages/otlp-exporter-base/test/common/configuration/otlp-http-configuration.test.ts b/experimental/packages/otlp-exporter-base/test/common/configuration/otlp-http-configuration.test.ts new file mode 100644 index 0000000000..03d040b14f --- /dev/null +++ b/experimental/packages/otlp-exporter-base/test/common/configuration/otlp-http-configuration.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { + mergeOtlpHttpConfigurationWithDefaults, + OtlpHttpConfiguration, +} from '../../../src/configuration/otlp-http-configuration'; +import * as assert from 'assert'; +import { testSharedConfigBehavior } from './shared-configuration.test'; + +describe('mergeOtlpHttpConfigurationWithDefaults', function () { + const testDefaults: OtlpHttpConfiguration = { + url: 'http://default.example.test', + timeoutMillis: 1, + compression: 'none', + concurrencyLimit: 2, + headers: { 'User-Agent': 'default-user-agent' }, + }; + + describe('headers', function () { + it('merges headers instead of overriding', function () { + const config = mergeOtlpHttpConfigurationWithDefaults( + { + headers: { foo: 'user' }, + }, + { + headers: { foo: 'fallback', bar: 'fallback' }, + }, + testDefaults + ); + assert.deepStrictEqual(config.headers, { + 'User-Agent': 'default-user-agent', + foo: 'user', + bar: 'fallback', + }); + }); + + it('does not override default (required) header', function () { + const config = mergeOtlpHttpConfigurationWithDefaults( + { + headers: { 'User-Agent': 'custom' }, + }, + { + headers: { 'User-Agent': 'custom-fallback' }, + }, + testDefaults + ); + assert.deepStrictEqual(config.headers, { + 'User-Agent': 'default-user-agent', + }); + }); + + it('drops user-provided headers with undefined values', function () { + const config = mergeOtlpHttpConfigurationWithDefaults( + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore simulating plain JavaScript usage, ignoring types + headers: { foo: 'foo-user-provided', bar: undefined, baz: null }, + }, + {}, + testDefaults + ); + assert.deepStrictEqual(config.headers, { + foo: 'foo-user-provided', + baz: 'null', + 'User-Agent': 'default-user-agent', + }); + }); + }); + + describe('url', function () { + it('uses user provided url over fallback', function () { + const config = mergeOtlpHttpConfigurationWithDefaults( + { + url: 'https://example.test/user-provided', + }, + { + url: 'https://example.test/fallback', + }, + testDefaults + ); + assert.strictEqual(config.url, 'https://example.test/user-provided'); + }); + + it('uses fallback url over default', function () { + const config = mergeOtlpHttpConfigurationWithDefaults( + {}, + { + url: 'https://example.test/fallback', + }, + testDefaults + ); + assert.strictEqual(config.url, 'https://example.test/fallback'); + }); + + it('uses default if none other are specified', function () { + const config = mergeOtlpHttpConfigurationWithDefaults( + {}, + {}, + testDefaults + ); + assert.strictEqual(config.url, testDefaults.url); + }); + + it('throws error when the user-provided url is not parseable', function () { + assert.throws(() => { + mergeOtlpHttpConfigurationWithDefaults( + { url: 'this is not a URL' }, + {}, + testDefaults + ); + }, new Error("Configuration: Could not parse user-provided export URL: 'this is not a URL'")); + }); + }); + + testSharedConfigBehavior( + mergeOtlpHttpConfigurationWithDefaults, + testDefaults + ); +}); diff --git a/experimental/packages/otlp-exporter-base/test/common/configuration/shared-configuration.test.ts b/experimental/packages/otlp-exporter-base/test/common/configuration/shared-configuration.test.ts new file mode 100644 index 0000000000..413ec5e5e6 --- /dev/null +++ b/experimental/packages/otlp-exporter-base/test/common/configuration/shared-configuration.test.ts @@ -0,0 +1,136 @@ +/* + * 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 { + mergeOtlpSharedConfigurationWithDefaults, + OtlpSharedConfiguration, +} from '../../../src'; + +export function testSharedConfigBehavior( + sut: ( + userProvidedConfiguration: Partial, + fallbackConfiguration: Partial, + defaultConfiguration: T + ) => OtlpSharedConfiguration, + defaults: T +): void { + // copy so that we don't modify the original and pollute other tests. + const testDefaults = Object.assign({}, defaults); + + testDefaults.timeoutMillis = 1; + testDefaults.compression = 'none'; + testDefaults.concurrencyLimit = 2; + + describe('timeout', function () { + it('uses user provided timeout over fallback', () => { + const config = sut( + { + timeoutMillis: 222, + }, + { + timeoutMillis: 333, + }, + testDefaults + ); + assert.deepEqual(config.timeoutMillis, 222); + }); + it('uses fallback timeout over default', () => { + const config = sut( + {}, + { + timeoutMillis: 444, + }, + testDefaults + ); + assert.deepEqual(config.timeoutMillis, 444); + }); + it('uses default if none other are specified', () => { + const config = sut({}, {}, testDefaults); + assert.deepEqual(config.timeoutMillis, testDefaults.timeoutMillis); + }); + + it('throws when value is negative', function () { + assert.throws(() => sut({ timeoutMillis: -1 }, {}, testDefaults)); + }); + }); + + describe('compression', function () { + it('uses user provided compression over fallback', () => { + const config = sut( + { + compression: 'gzip', + }, + { + compression: 'none', + }, + testDefaults + ); + assert.deepEqual(config.compression, 'gzip'); + }); + it('uses fallback compression over default', () => { + const config = sut( + {}, + { + compression: 'gzip', + }, + testDefaults + ); + assert.deepEqual(config.compression, 'gzip'); + }); + it('uses default if none other are specified', () => { + const config = sut({}, {}, testDefaults); + assert.deepEqual(config.compression, testDefaults.compression); + }); + }); + + describe('concurrency limit', function () { + it('uses user provided limit over fallback', () => { + const config = sut( + { + concurrencyLimit: 20, + }, + { + concurrencyLimit: 40, + }, + testDefaults + ); + assert.deepEqual(config.concurrencyLimit, 20); + }); + it('uses fallback limit over default', () => { + const config = sut( + {}, + { + concurrencyLimit: 50, + }, + testDefaults + ); + assert.deepEqual(config.concurrencyLimit, 50); + }); + it('uses default if none other are specified', () => { + const config = sut({}, {}, testDefaults); + assert.deepEqual(config.concurrencyLimit, testDefaults.concurrencyLimit); + }); + }); +} + +describe('mergeOtlpSharedConfigurationWithDefaults', function () { + testSharedConfigBehavior(mergeOtlpSharedConfigurationWithDefaults, { + timeoutMillis: 1, + compression: 'none', + concurrencyLimit: 2, + }); +}); diff --git a/experimental/packages/otlp-exporter-base/test/common/is-export-retryable.test.ts b/experimental/packages/otlp-exporter-base/test/common/is-export-retryable.test.ts new file mode 100644 index 0000000000..458bf75a76 --- /dev/null +++ b/experimental/packages/otlp-exporter-base/test/common/is-export-retryable.test.ts @@ -0,0 +1,45 @@ +/* + * 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 sinon from 'sinon'; +import * as assert from 'assert'; +import { parseRetryAfterToMills } from '../../src/is-export-retryable'; + +describe('parseRetryAfterToMills', function () { + // now: 2023-01-20T00:00:00.000Z + const tests = [ + [null, undefined], + // duration + ['-100', -1], + ['1000', 1000 * 1000], + // future timestamp + ['Fri, 20 Jan 2023 00:00:01 GMT', 1000], + // Past timestamp + ['Fri, 19 Jan 2023 23:59:59 GMT', 0], + ] as [string | null, number][]; + + afterEach(() => { + sinon.restore(); + }); + + for (const [value, expect] of tests) { + it(`test ${value}`, () => { + sinon.useFakeTimers({ + now: new Date('2023-01-20T00:00:00.000Z'), + }); + assert.strictEqual(parseRetryAfterToMills(value), expect); + }); + } +}); diff --git a/experimental/packages/otlp-exporter-base/test/common/util.test.ts b/experimental/packages/otlp-exporter-base/test/common/util.test.ts index 4448ec06da..3bd00a2f22 100644 --- a/experimental/packages/otlp-exporter-base/test/common/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/common/util.test.ts @@ -17,131 +17,34 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { diag } from '@opentelemetry/api'; -import { - parseHeaders, - appendResourcePathToUrl, - appendRootPathToUrlIfNeeded, - parseRetryAfterToMills, -} from '../../src/util'; +import { validateAndNormalizeHeaders } from '../../src/util'; -describe('utils', () => { - afterEach(() => { +describe('parseHeaders', function () { + afterEach(function () { sinon.restore(); }); - - describe('parseHeaders', () => { - it('should ignore undefined headers', () => { - // Need to stub/spy on the underlying logger as the "diag" instance is global - const spyWarn = sinon.stub(diag, 'warn'); - const headers: Partial> = { - foo1: undefined, - foo2: 'bar', - foo3: 1, - }; - const result = parseHeaders(headers); - assert.deepStrictEqual(result, { - foo2: 'bar', - foo3: '1', - }); - const args = spyWarn.args[0]; - assert.strictEqual( - args[0], - 'Header "foo1" has invalid value (undefined) and will be ignored' - ); - }); - - it('should parse undefined', () => { - const result = parseHeaders(undefined); - assert.deepStrictEqual(result, {}); - }); + it('should ignore undefined headers', function () { + // Need to stub/spy on the underlying logger as the "diag" instance is global + const spyWarn = sinon.stub(diag, 'warn'); + const headers: Partial> = { + foo1: undefined, + foo2: 'bar', + foo3: 1, + }; + const result = validateAndNormalizeHeaders(headers); + assert.deepStrictEqual(result, { + foo2: 'bar', + foo3: '1', + }); + const args = spyWarn.args[0]; + assert.strictEqual( + args[0], + 'Header "foo1" has invalid value (undefined) and will be ignored' + ); }); - // only invoked with general endpoint (not signal specific endpoint) - describe('appendResourcePathToUrl - general http endpoint', () => { - it('should append resource path when missing', () => { - const url = 'http://foo.bar/'; - const resourcePath = 'v1/traces'; - - const finalUrl = appendResourcePathToUrl(url, resourcePath); - assert.strictEqual(finalUrl, url + resourcePath); - }); - it('should append root path and resource path when missing', () => { - const url = 'http://foo.bar'; - const resourcePath = 'v1/traces'; - - const finalUrl = appendResourcePathToUrl(url, resourcePath); - assert.strictEqual(finalUrl, url + '/' + resourcePath); - }); - it('should append resource path even when url already contains path ', () => { - const url = 'http://foo.bar/v1/traces'; - const resourcePath = 'v1/traces'; - - const finalUrl = appendResourcePathToUrl(url, resourcePath); - assert.strictEqual(finalUrl, url + '/' + resourcePath); - }); + it('should parse undefined', function () { + const result = validateAndNormalizeHeaders(undefined); + assert.deepStrictEqual(result, {}); }); - - // only invoked with signal specific endpoint - describe('appendRootPathToUrlIfNeeded - specific signal http endpoint', () => { - it('should append root path when missing', () => { - const url = 'http://foo.bar'; - - const finalUrl = appendRootPathToUrlIfNeeded(url); - assert.strictEqual(finalUrl, url + '/'); - }); - it('should not append root path and return same url', () => { - const url = 'http://foo.bar/'; - - const finalUrl = appendRootPathToUrlIfNeeded(url); - assert.strictEqual(finalUrl, url); - }); - it('should not append root path when url contains resource path', () => { - { - const url = 'http://foo.bar/v1/traces'; - - const finalUrl = appendRootPathToUrlIfNeeded(url); - assert.strictEqual(finalUrl, url); - } - { - const url = 'https://endpoint/something'; - - const finalUrl = appendRootPathToUrlIfNeeded(url); - assert.strictEqual(finalUrl, url); - } - }); - - it('should not change string when url is not parsable', () => { - const url = 'this is not a URL'; - - const finalUrl = appendRootPathToUrlIfNeeded(url); - assert.strictEqual(finalUrl, url); - }); - }); -}); - -describe('parseRetryAfterToMills', () => { - // now: 2023-01-20T00:00:00.000Z - const tests = [ - [null, -1], - // duration - ['-100', -1], - ['1000', 1000 * 1000], - // future timestamp - ['Fri, 20 Jan 2023 00:00:01 GMT', 1000], - // Past timestamp - ['Fri, 19 Jan 2023 23:59:59 GMT', 0], - ] as [string | null, number][]; - - afterEach(() => { - sinon.restore(); - }); - - for (const [value, expect] of tests) { - it(`test ${value}`, () => { - sinon.useFakeTimers({ - now: new Date('2023-01-20T00:00:00.000Z'), - }); - assert.strictEqual(parseRetryAfterToMills(value), expect); - }); - } }); diff --git a/experimental/packages/otlp-exporter-base/test/node/configuration/otlp-http-env-configuration.test.ts b/experimental/packages/otlp-exporter-base/test/node/configuration/otlp-http-env-configuration.test.ts new file mode 100644 index 0000000000..5b4bc4dc47 --- /dev/null +++ b/experimental/packages/otlp-exporter-base/test/node/configuration/otlp-http-env-configuration.test.ts @@ -0,0 +1,244 @@ +/* + * 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 { getHttpConfigurationFromEnvironment } from '../../../src/configuration/otlp-http-env-configuration'; +import { testSharedConfigurationFromEnvironment } from './shared-env-configuration.test'; + +describe('getHttpConfigurationFromEnvironment', function () { + describe('headers', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_HEADERS; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS; + }); + + it('unset if env vars are not set', function () { + // ensure both are not set + delete process.env.OTEL_EXPORTER_OTLP_HEADERS; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS; + + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual(config.headers, undefined); + }); + + it('merges headers instead of overriding', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = 'key1=value1,key2=value2'; + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = 'key1=metrics'; + + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.deepEqual(config.headers, { + key1: 'metrics', + key2: 'value2', + }); + }); + + it('allows non-specific only headers', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = 'key1=value1,key2=value2'; + + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.deepEqual(config.headers, { + key1: 'value1', + key2: 'value2', + }); + }); + + it('allows specific only headers', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'key1=value1,key2=value2'; + + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.deepEqual(config.headers, { + key1: 'value1', + key2: 'value2', + }); + }); + + it('remains unset if specific headers are lists of empty strings', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ' , , ,'; + + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.equal(config.headers, undefined); + }); + }); + + describe('url', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT; + sinon.restore(); + }); + + it('should use url defined in env that ends with root path and append version and signal path', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}v1/metrics` + ); + }); + + it('should use url defined in env without checking if path is already present', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/v1/metrics'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics` + ); + }); + + it('should use url defined in env and append version and signal', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT}/v1/metrics` + ); + }); + + it('should override global exporter url with signal url defined in env', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://foo.bar/'; + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.metrics/'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + ); + }); + + it('should add root path when signal url defined in env contains no path and no root path', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.bar'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}/` + ); + }); + + it('should not add root path when signal url defined in env contains root path but no path', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://foo.bar/'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` + ); + }); + + it('should not add root path when signal url defined in env contains path', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = + 'http://foo.bar/v1/metrics'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` + ); + }); + + it('should not add root path when signal url defined in env contains path and ends in /', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = + 'http://foo.bar/v1/metrics/'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual( + config.url, + `${process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT}` + ); + }); + + it('should warn on invalid specific url', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'not a url'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual(config.url, undefined); + sinon.assert.calledOnceWithExactly( + spyLoggerWarn, + "Configuration: Could not parse environment-provided export URL: 'not a url', falling back to undefined" + ); + }); + + it('should warn on invalid non-specific url', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'not a url'; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual(config.url, undefined); + sinon.assert.calledOnceWithExactly( + spyLoggerWarn, + "Configuration: Could not parse environment-provided export URL: 'not a url', falling back to undefined" + ); + }); + + it('should treat empty urls as not set', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ''; + const config = getHttpConfigurationFromEnvironment( + 'METRICS', + 'v1/metrics' + ); + assert.strictEqual(config.url, undefined); + }); + }); + + testSharedConfigurationFromEnvironment(signalIdentifier => + getHttpConfigurationFromEnvironment(signalIdentifier, 'v1/metrics') + ); +}); diff --git a/experimental/packages/otlp-exporter-base/test/node/configuration/shared-env-configuration.test.ts b/experimental/packages/otlp-exporter-base/test/node/configuration/shared-env-configuration.test.ts new file mode 100644 index 0000000000..51d55d4980 --- /dev/null +++ b/experimental/packages/otlp-exporter-base/test/node/configuration/shared-env-configuration.test.ts @@ -0,0 +1,208 @@ +/* + * 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 * as process from 'process'; +import { + getSharedConfigurationFromEnvironment, + OtlpSharedConfiguration, +} from '../../../src'; + +export function testSharedConfigurationFromEnvironment( + sut: (signalIdentifier: string) => Partial +): void { + describe('timeout', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_TIMEOUT; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT; + sinon.restore(); + }); + + it('should not define timeoutMillis if no env var is set', function () { + const config = sut('METRICS'); + assert.strictEqual(config.timeoutMillis, undefined); + }); + + it('should use specific timeout value', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = '15000'; + const config = sut('METRICS'); + assert.strictEqual(config.timeoutMillis, 15000); + }); + + it('should not define timeoutMillis when specific timeout value is negative', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = '-15000'; + const config = sut('METRICS'); + assert.strictEqual(config.timeoutMillis, undefined); + }); + + it('should not define timeoutMillis when specific and non-specific timeout values are negative', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = '-11000'; + process.env.OTEL_EXPORTER_OTLP_TIMEOUT = '-9000'; + + const config = sut('METRICS'); + + sinon.assert.calledTwice(spyLoggerWarn); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Configuration: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT is invalid, expected number greater than 0 (actual: -11000)' + ); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Configuration: OTEL_EXPORTER_OTLP_TIMEOUT is invalid, expected number greater than 0 (actual: -9000)' + ); + + assert.strictEqual(config.timeoutMillis, undefined); + }); + + it('should not define timeoutMillis when specific and non-specific timeout values are NaN', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = 'NaN'; + process.env.OTEL_EXPORTER_OTLP_TIMEOUT = 'foo'; + + const config = sut('METRICS'); + + sinon.assert.calledTwice(spyLoggerWarn); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Configuration: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT is invalid, expected number greater than 0 (actual: NaN)' + ); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Configuration: OTEL_EXPORTER_OTLP_TIMEOUT is invalid, expected number greater than 0 (actual: foo)' + ); + assert.strictEqual(config.timeoutMillis, undefined); + }); + it('should not define timeoutMillis when specific and non-specific timeout values are infinite', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = '-Infinitiy'; + process.env.OTEL_EXPORTER_OTLP_TIMEOUT = 'Infinity'; + + const config = sut('METRICS'); + + sinon.assert.calledTwice(spyLoggerWarn); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Configuration: OTEL_EXPORTER_OTLP_METRICS_TIMEOUT is invalid, expected number greater than 0 (actual: -Infinitiy)' + ); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Configuration: OTEL_EXPORTER_OTLP_TIMEOUT is invalid, expected number greater than 0 (actual: Infinity)' + ); + assert.strictEqual(config.timeoutMillis, undefined); + }); + + it('should not define timeoutMillis when specific and non-specific timeout values are empty strings', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = ''; + process.env.OTEL_EXPORTER_OTLP_TIMEOUT = ''; + + const config = sut('METRICS'); + + sinon.assert.notCalled(spyLoggerWarn); + assert.strictEqual(config.timeoutMillis, undefined); + }); + + it('should not define timeoutMillis when specific and non-specific timeout values are blank strings', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT = ' '; + process.env.OTEL_EXPORTER_OTLP_TIMEOUT = ' '; + + const config = sut('METRICS'); + + sinon.assert.notCalled(spyLoggerWarn); + assert.strictEqual(config.timeoutMillis, undefined); + }); + }); + + describe('compression', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_COMPRESSION; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_COMPRESSION; + sinon.restore(); + }); + + it('should not define compression if no env var is set', function () { + const config = sut('METRICS'); + assert.strictEqual(config.compression, undefined); + }); + + it('should use specific compression value', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = 'gzip'; + const config = sut('METRICS'); + assert.strictEqual(config.compression, 'gzip'); + }); + + it('should not define when specific compression value is invalid', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = 'bla'; + const config = sut('METRICS'); + sinon.assert.calledOnceWithExactly( + spyLoggerWarn, + "Configuration: OTEL_EXPORTER_OTLP_METRICS_COMPRESSION is invalid, expected 'none' or 'gzip' (actual: 'bla')" + ); + assert.strictEqual(config.compression, undefined); + }); + + it('should not define when non-specific compression value is invalid', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_COMPRESSION = 'bla'; + const config = sut('METRICS'); + sinon.assert.calledOnceWithExactly( + spyLoggerWarn, + "Configuration: OTEL_EXPORTER_OTLP_COMPRESSION is invalid, expected 'none' or 'gzip' (actual: 'bla')" + ); + assert.strictEqual(config.compression, undefined); + }); + + it('should use signal specific over non-specific', function () { + process.env.OTEL_EXPORTER_OTLP_COMPRESSION = 'none'; + process.env.OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = 'gzip'; + + const config = sut('METRICS'); + + assert.strictEqual(config.compression, 'gzip'); + }); + + it('should treat empty string values as undefined', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_COMPRESSION = ''; + process.env.OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = ''; + + const config = sut('METRICS'); + + sinon.assert.notCalled(spyLoggerWarn); + assert.strictEqual(config.compression, undefined); + }); + + it('should use fallback if value is blank', function () { + const spyLoggerWarn = sinon.stub(diag, 'warn'); + process.env.OTEL_EXPORTER_OTLP_COMPRESSION = 'gzip'; + process.env.OTEL_EXPORTER_OTLP_METRICS_COMPRESSION = ' '; + + const config = sut('METRICS'); + + sinon.assert.notCalled(spyLoggerWarn); + assert.strictEqual(config.compression, 'gzip'); + }); + }); +} + +describe('getSharedConfigurationFromEnvironment', function () { + testSharedConfigurationFromEnvironment(getSharedConfigurationFromEnvironment); +}); diff --git a/experimental/packages/otlp-exporter-base/test/node/util.test.ts b/experimental/packages/otlp-exporter-base/test/node/util.test.ts index 19abbee952..93b70db197 100644 --- a/experimental/packages/otlp-exporter-base/test/node/util.test.ts +++ b/experimental/packages/otlp-exporter-base/test/node/util.test.ts @@ -14,24 +14,10 @@ * limitations under the License. */ -import * as assert from 'assert'; -import { configureExporterTimeout, invalidTimeout } from '../../src/util'; -import { CompressionAlgorithm } from '../../src/platform/node/types'; -import { configureCompression } from '../../src/platform/node/util'; -import { diag } from '@opentelemetry/api'; -import * as sinon from 'sinon'; - import { OTLPExporterNodeBase } from '../../src/platform/node/OTLPExporterNodeBase'; -import { OTLPExporterNodeConfigBase } from '../../src/platform/node/types'; import { ISerializer } from '@opentelemetry/otlp-transformer'; -// Barebones exporter for use by sendWithHttp -type ExporterConfig = OTLPExporterNodeConfigBase; -class Exporter extends OTLPExporterNodeBase { - getDefaultUrl(config: ExporterConfig): string { - return config.url || ''; - } -} +class Exporter extends OTLPExporterNodeBase {} const noopSerializer: ISerializer = { serializeRequest(request: object): Uint8Array | undefined { @@ -44,123 +30,7 @@ const noopSerializer: ISerializer = { describe('force flush', () => { it('forceFlush should flush spans and return', async () => { - const exporter = new Exporter({}, noopSerializer, {}); + const exporter = new Exporter({}, noopSerializer, {}, 'TEST', 'v1/test'); await exporter.forceFlush(); }); }); - -describe('configureExporterTimeout', () => { - const envSource = process.env; - it('should use timeoutMillis parameter as export timeout value', () => { - const exporterTimeout = configureExporterTimeout(9000); - assert.strictEqual(exporterTimeout, 9000); - }); - it('should use default trace export timeout env variable value when timeoutMillis parameter is undefined', () => { - const exporterTimeout = configureExporterTimeout(undefined); - assert.strictEqual(exporterTimeout, 10000); - }); - it('should use default trace export timeout env variable value when timeoutMillis parameter is negative', () => { - const exporterTimeout = configureExporterTimeout(-18000); - assert.strictEqual(exporterTimeout, 10000); - }); - it('should use trace export timeout value defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = '15000'; - const exporterTimeout = configureExporterTimeout(undefined); - assert.strictEqual(exporterTimeout, 15000); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT; - }); - it('should use default trace export timeout env variable value when trace export timeout value defined in env is negative', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = '-15000'; - const exporterTimeout = configureExporterTimeout(undefined); - assert.strictEqual(exporterTimeout, 10000); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT; - }); - it('should use default trace export timeout when timeoutMillis parameter is negative', () => { - const exporterTimeout = configureExporterTimeout(-15000); - assert.strictEqual(exporterTimeout, 10000); - }); - it('should use timeoutMillis parameter over trace export timeout value defined in env', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = '11000'; - const exporterTimeout = configureExporterTimeout(9000); - assert.strictEqual(exporterTimeout, 9000); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT; - }); - it('should use default value when both timeoutMillis parameter and export timeout values defined in env are negative', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = '-11000'; - envSource.OTEL_EXPORTER_OTLP_TIMEOUT = '-9000'; - const exporterTimeout = configureExporterTimeout(-5000); - assert.strictEqual(exporterTimeout, 10000); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT; - delete envSource.OTEL_EXPORTER_OTLP_TIMEOUT; - }); - it('should use default value export timeout value defined in env are negative', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT = '-11000'; - envSource.OTEL_EXPORTER_OTLP_TIMEOUT = '-9000'; - const exporterTimeout = configureExporterTimeout(undefined); - assert.strictEqual(exporterTimeout, 10000); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_TIMEOUT; - delete envSource.OTEL_EXPORTER_OTLP_TIMEOUT; - }); - it('should warn user about invalid timeout', () => { - const spyLoggerWarn = sinon.stub(diag, 'warn'); - configureExporterTimeout(-15000); - const args = spyLoggerWarn.args[0]; - assert.strictEqual(args[0], 'Timeout must be greater than 0'); - assert.strictEqual(args[1], -15000); - sinon.restore(); - }); -}); - -describe('invalidTimeout', () => { - it('should warn user about invalid timeout', () => { - const spyLoggerWarn = sinon.stub(diag, 'warn'); - invalidTimeout(-9000, 10000); - const args = spyLoggerWarn.args[0]; - assert.strictEqual(args[0], 'Timeout must be greater than 0'); - assert.strictEqual(args[1], -9000); - sinon.restore(); - }); - it('diag warn was called', () => { - const spyLoggerWarn = sinon.stub(diag, 'warn'); - invalidTimeout(-9000, 10000); - assert(spyLoggerWarn.calledOnce); - sinon.restore(); - }); - it('should return default timeout', () => { - const defaultTimeout = invalidTimeout(-9000, 10000); - assert.strictEqual(defaultTimeout, 10000); - }); -}); - -describe('configureCompression', () => { - const envSource = process.env; - it('should return none for compression', () => { - const compression = CompressionAlgorithm.NONE; - assert.strictEqual( - configureCompression(compression), - CompressionAlgorithm.NONE - ); - }); - it('should return gzip compression defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = 'gzip'; - assert.strictEqual( - configureCompression(undefined), - CompressionAlgorithm.GZIP - ); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION; - }); - it('should return none for compression defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = 'none'; - assert.strictEqual( - configureCompression(undefined), - CompressionAlgorithm.NONE - ); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION; - }); - it('should return none for compression when no compression is set', () => { - assert.strictEqual( - configureCompression(undefined), - CompressionAlgorithm.NONE - ); - }); -}); diff --git a/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts b/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts index 6ad33ec77c..e2ba61779b 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/OTLPGRPCExporterNodeBase.ts @@ -15,10 +15,8 @@ */ import { diag } from '@opentelemetry/api'; -import { GRPCQueueItem, OTLPGRPCExporterConfigNode } from './types'; -import { baggageUtils, getEnv } from '@opentelemetry/core'; +import { OTLPGRPCExporterConfigNode } from './types'; import { - CompressionAlgorithm, OTLPExporterBase, OTLPExporterError, } from '@opentelemetry/otlp-exporter-base'; @@ -26,9 +24,13 @@ import { createEmptyMetadata, GrpcExporterTransport, } from './grpc-exporter-transport'; -import { configureCompression, configureCredentials } from './util'; import { ISerializer } from '@opentelemetry/otlp-transformer'; import { IExporterTransport } from '@opentelemetry/otlp-exporter-base'; +import { + getOtlpGrpcDefaultConfiguration, + mergeOtlpGrpcConfigurationWithDefaults, +} from './configuration/otlp-grpc-configuration'; +import { getOtlpGrpcConfigurationFromEnv } from './configuration/otlp-grpc-env-configuration'; /** * OTLP Exporter abstract base class @@ -37,73 +39,55 @@ export abstract class OTLPGRPCExporterNodeBase< ExportItem, ServiceResponse, > extends OTLPExporterBase { - grpcQueue: GRPCQueueItem[] = []; - compression: CompressionAlgorithm; private _transport: IExporterTransport; private _serializer: ISerializer; + private _timeoutMillis: number; constructor( config: OTLPGRPCExporterConfigNode = {}, - signalSpecificMetadata: Record, + serializer: ISerializer, grpcName: string, grpcPath: string, - serializer: ISerializer + signalIdentifier: string ) { super(config); + // keep credentials locally in case user updates the reference on the config object + const userProvidedCredentials = config.credentials; + const actualConfig = mergeOtlpGrpcConfigurationWithDefaults( + { + url: config.url, + metadata: () => { + // metadata resolution strategy is merge, so we can return empty here, and it will not override the rest of the settings. + return config.metadata ?? createEmptyMetadata(); + }, + compression: config.compression, + timeoutMillis: config.timeoutMillis, + concurrencyLimit: config.concurrencyLimit, + credentials: + userProvidedCredentials != null + ? () => userProvidedCredentials + : undefined, + }, + getOtlpGrpcConfigurationFromEnv(signalIdentifier), + getOtlpGrpcDefaultConfiguration() + ); this._serializer = serializer; + this._timeoutMillis = actualConfig.timeoutMillis; + this._concurrencyLimit = actualConfig.concurrencyLimit; if (config.headers) { diag.warn('Headers cannot be set when using grpc'); } - const nonSignalSpecificMetadata = baggageUtils.parseKeyPairsIntoRecord( - getEnv().OTEL_EXPORTER_OTLP_HEADERS - ); - const rawMetadata = Object.assign( - {}, - nonSignalSpecificMetadata, - signalSpecificMetadata - ); - - let credentialProvider = () => { - return configureCredentials(undefined, this.getUrlFromConfig(config)); - }; - - if (config.credentials != null) { - const credentials = config.credentials; - credentialProvider = () => { - return credentials; - }; - } - // Ensure we don't modify the original. - const configMetadata = config.metadata?.clone(); - const metadataProvider = () => { - const metadata = configMetadata ?? createEmptyMetadata(); - for (const [key, value] of Object.entries(rawMetadata)) { - // only override with env var data if the key has no values. - // not using Metadata.merge() as it will keep both values. - if (metadata.get(key).length < 1) { - metadata.set(key, value); - } - } - - return metadata; - }; - - this.compression = configureCompression(config.compression); this._transport = new GrpcExporterTransport({ - address: this.getDefaultUrl(config), - compression: this.compression, - credentials: credentialProvider, + address: actualConfig.url, + compression: actualConfig.compression, + credentials: actualConfig.credentials, grpcName: grpcName, grpcPath: grpcPath, - metadata: metadataProvider, + metadata: actualConfig.metadata, }); } - onInit() { - // Intentionally left empty; nothing to do. - } - override onShutdown() { this._transport.shutdown(); } @@ -126,7 +110,7 @@ export abstract class OTLPGRPCExporterNodeBase< } const promise = this._transport - .send(data, this.timeoutMillis) + .send(data, this._timeoutMillis) .then(response => { if (response.status === 'success') { onSuccess(); @@ -146,6 +130,4 @@ export abstract class OTLPGRPCExporterNodeBase< }; promise.then(popPromise, popPromise); } - - abstract getUrlFromConfig(config: OTLPGRPCExporterConfigNode): string; } diff --git a/experimental/packages/otlp-grpc-exporter-base/src/configuration/otlp-grpc-configuration.ts b/experimental/packages/otlp-grpc-exporter-base/src/configuration/otlp-grpc-configuration.ts new file mode 100644 index 0000000000..af757e3f03 --- /dev/null +++ b/experimental/packages/otlp-grpc-exporter-base/src/configuration/otlp-grpc-configuration.ts @@ -0,0 +1,141 @@ +/* + * 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 { + getSharedConfigurationDefaults, + mergeOtlpSharedConfigurationWithDefaults, + OtlpSharedConfiguration, +} from '@opentelemetry/otlp-exporter-base'; +import { + createEmptyMetadata, + createInsecureCredentials, + createSslCredentials, +} from '../grpc-exporter-transport'; +import { VERSION } from '../version'; +import { URL } from 'url'; +import { diag } from '@opentelemetry/api'; + +// NOTE: do not change this to be an actual import, doing so will break `@opentelemetry/instrumentation-grpc` +import type { ChannelCredentials, Metadata } from '@grpc/grpc-js'; + +export interface OtlpGrpcConfiguration extends OtlpSharedConfiguration { + url: string; + metadata: () => Metadata; + credentials: () => ChannelCredentials; +} + +/** + * Unresolved configuration where parts of the config may depend on other config options being resolved first. + */ +export interface UnresolvedOtlpGrpcConfiguration + extends OtlpSharedConfiguration { + url: string; + metadata: () => Metadata; + /** + * Credentials are based on the final resolved URL + */ + credentials: (url: string) => () => ChannelCredentials; +} + +export function validateAndNormalizeUrl(url: string): string { + url = url.trim(); + const hasProtocol = url.match(/^([\w]{1,8}):\/\//); + if (!hasProtocol) { + url = `https://${url}`; + } + const target = new URL(url); + if (target.protocol === 'unix:') { + return url; + } + if (target.pathname && target.pathname !== '/') { + diag.warn( + 'URL path should not be set when using grpc, the path part of the URL will be ignored.' + ); + } + if (target.protocol !== '' && !target.protocol?.match(/^(http)s?:$/)) { + diag.warn('URL protocol should be http(s)://. Using http://.'); + } + return target.host; +} + +function overrideMetadataEntriesIfNotPresent( + metadata: Metadata, + additionalMetadata: Metadata +): void { + for (const [key, value] of Object.entries(additionalMetadata.getMap())) { + // only override with env var data if the key has no values. + // not using Metadata.merge() as it will keep both values. + if (metadata.get(key).length < 1) { + metadata.set(key, value); + } + } +} + +export function mergeOtlpGrpcConfigurationWithDefaults( + userProvidedConfiguration: Partial, + fallbackConfiguration: Partial, + defaultConfiguration: UnresolvedOtlpGrpcConfiguration +): OtlpGrpcConfiguration { + const rawUrl = + userProvidedConfiguration.url ?? + fallbackConfiguration.url ?? + defaultConfiguration.url; + + return { + ...mergeOtlpSharedConfigurationWithDefaults( + userProvidedConfiguration, + fallbackConfiguration, + defaultConfiguration + ), + metadata: () => { + const metadata = defaultConfiguration.metadata(); + overrideMetadataEntriesIfNotPresent( + metadata, + // clone to ensure we don't modify what the user gave us in case they hold on to the returned reference + userProvidedConfiguration.metadata?.().clone() ?? createEmptyMetadata() + ); + overrideMetadataEntriesIfNotPresent( + metadata, + fallbackConfiguration.metadata?.() ?? createEmptyMetadata() + ); + return metadata; + }, + url: validateAndNormalizeUrl(rawUrl), + credentials: + userProvidedConfiguration.credentials ?? + fallbackConfiguration.credentials?.(rawUrl) ?? + defaultConfiguration.credentials(rawUrl), + }; +} + +export function getOtlpGrpcDefaultConfiguration(): UnresolvedOtlpGrpcConfiguration { + return { + ...getSharedConfigurationDefaults(), + metadata: () => { + const metadata = createEmptyMetadata(); + metadata.set('User-Agent', `OTel-OTLP-Exporter-JavaScript/${VERSION}`); + return metadata; + }, + url: 'http://localhost:4317', + credentials: (url: string) => { + if (url.startsWith('http://')) { + return () => createInsecureCredentials(); + } else { + return () => createSslCredentials(); + } + }, + }; +} diff --git a/experimental/packages/otlp-grpc-exporter-base/src/configuration/otlp-grpc-env-configuration.ts b/experimental/packages/otlp-grpc-exporter-base/src/configuration/otlp-grpc-env-configuration.ts new file mode 100644 index 0000000000..6ad8926604 --- /dev/null +++ b/experimental/packages/otlp-grpc-exporter-base/src/configuration/otlp-grpc-env-configuration.ts @@ -0,0 +1,259 @@ +/* + * 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 { UnresolvedOtlpGrpcConfiguration } from './otlp-grpc-configuration'; +import type { ChannelCredentials, Metadata } from '@grpc/grpc-js'; +import { baggageUtils } from '@opentelemetry/core'; +import { + createEmptyMetadata, + createInsecureCredentials, + createSslCredentials, +} from '../grpc-exporter-transport'; +import { getSharedConfigurationFromEnvironment } from '@opentelemetry/otlp-exporter-base'; +import * as fs from 'fs'; +import * as path from 'path'; +import { diag } from '@opentelemetry/api'; + +function fallbackIfNullishOrBlank( + signalSpecific?: string, + nonSignalSpecific?: string +): string | undefined { + if (signalSpecific != null && signalSpecific !== '') { + return signalSpecific; + } + + if (nonSignalSpecific != null && nonSignalSpecific !== '') { + return nonSignalSpecific; + } + + return undefined; +} + +function getMetadataFromEnv(signalIdentifier: string): Metadata | undefined { + const signalSpecificRawHeaders = + process.env[`OTEL_EXPORTER_OTLP_${signalIdentifier}_HEADERS`]?.trim(); + const nonSignalSpecificRawHeaders = + process.env['OTEL_EXPORTER_OTLP_HEADERS']?.trim(); + + const signalSpecificHeaders = baggageUtils.parseKeyPairsIntoRecord( + signalSpecificRawHeaders + ); + const nonSignalSpecificHeaders = baggageUtils.parseKeyPairsIntoRecord( + nonSignalSpecificRawHeaders + ); + + if ( + Object.keys(signalSpecificHeaders).length === 0 && + Object.keys(nonSignalSpecificHeaders).length === 0 + ) { + return undefined; + } + + const mergeHeaders = Object.assign( + {}, + nonSignalSpecificHeaders, + signalSpecificHeaders + ); + + const metadata = createEmptyMetadata(); + + // for this to work, metadata MUST be empty - otherwise `Metadata#set()` will merge items. + for (const [key, value] of Object.entries(mergeHeaders)) { + metadata.set(key, value); + } + + return metadata; +} + +function getMetadataProviderFromEnv( + signalIdentifier: string +): (() => Metadata) | undefined { + const metadata = getMetadataFromEnv(signalIdentifier); + if (metadata == null) { + return undefined; + } + + return () => metadata; +} + +function getUrlFromEnv(signalIdentifier: string) { + // This does not change the string beyond trimming on purpose. + // Normally a user would just use a host and port for gRPC, but the OTLP Exporter specification requires us to + // use the raw provided endpoint to derive credential settings. Therefore, we only normalize right when + // we merge user-provided, env-provided and defaults together, and we have determined which credentials to use. + // + // Examples: + // - example.test:4317 -> use secure credentials from environment (or provided via code) + // - http://example.test:4317 -> use insecure credentials if nothing else is provided + // - https://example.test:4317 -> use secure credentials from environment (or provided via code) + + const specificEndpoint = + process.env[`OTEL_EXPORTER_OTLP_${signalIdentifier}_ENDPOINT`]?.trim(); + const nonSpecificEndpoint = + process.env[`OTEL_EXPORTER_OTLP_ENDPOINT`]?.trim(); + + return fallbackIfNullishOrBlank(specificEndpoint, nonSpecificEndpoint); +} + +/** + * Determines whether the env var for insecure credentials is set to {@code true}. + * + * It will allow the following values as {@code true} + * - 'true' + * - 'true ' + * - ' true' + * - 'TrUE' + * - 'TRUE' + * + * It will not allow: + * - 'true false' + * - 'false true' + * - 'true!' + * - 'true,true' + * - '1' + * - ' ' + * + * @param signalIdentifier + */ +function getInsecureSettingFromEnv(signalIdentifier: string): boolean { + const signalSpecificInsecureValue = process.env[ + `OTEL_EXPORTER_OTLP_${signalIdentifier}_INSECURE` + ] + ?.toLowerCase() + .trim(); + const nonSignalSpecificInsecureValue = process.env[ + `OTEL_EXPORTER_OTLP_INSECURE` + ] + ?.toLowerCase() + .trim(); + + return ( + fallbackIfNullishOrBlank( + signalSpecificInsecureValue, + nonSignalSpecificInsecureValue + ) === 'true' + ); +} + +function readFileFromEnv( + signalSpecificEnvVar: string, + nonSignalSpecificEnvVar: string, + warningMessage: string +): Buffer | undefined { + const signalSpecificPath = process.env[signalSpecificEnvVar]?.trim(); + const nonSignalSpecificPath = process.env[nonSignalSpecificEnvVar]?.trim(); + + const filePath = fallbackIfNullishOrBlank( + signalSpecificPath, + nonSignalSpecificPath + ); + + if (filePath != null) { + try { + return fs.readFileSync(path.resolve(process.cwd(), filePath)); + } catch { + diag.warn(warningMessage); + return undefined; + } + } else { + return undefined; + } +} + +function getClientCertificateFromEnv( + signalIdentifier: string +): Buffer | undefined { + return readFileFromEnv( + `OTEL_EXPORTER_OTLP_${signalIdentifier}_CLIENT_CERTIFICATE`, + 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE', + 'Failed to read client certificate chain file' + ); +} + +function getClientKeyFromEnv(signalIdentifier: string): Buffer | undefined { + return readFileFromEnv( + `OTEL_EXPORTER_OTLP_${signalIdentifier}_CLIENT_KEY`, + 'OTEL_EXPORTER_OTLP_CLIENT_KEY', + 'Failed to read client certificate private key file' + ); +} + +function getRootCertificateFromEnv( + signalIdentifier: string +): Buffer | undefined { + return readFileFromEnv( + `OTEL_EXPORTER_OTLP_${signalIdentifier}_CERTIFICATE`, + 'OTEL_EXPORTER_OTLP_CERTIFICATE', + 'Failed to read root certificate file' + ); +} + +function getCredentialsFromEnvIgnoreInsecure( + signalIdentifier: string +): ChannelCredentials { + const clientKey = getClientKeyFromEnv(signalIdentifier); + const clientCertificate = getClientCertificateFromEnv(signalIdentifier); + const rootCertificate = getRootCertificateFromEnv(signalIdentifier); + + // if the chain is not intact, @grpc/grpc-js will throw. This is fine when a user provides it in code, but env var + // config is not allowed to throw, so we add this safeguard and try to make the best of it here. + const clientChainIntact = clientKey != null && clientCertificate != null; + if (rootCertificate != null && !clientChainIntact) { + diag.warn( + 'Client key and certificate must both be provided, but one was missing - attempting to create credentials from just the root certificate' + ); + return createSslCredentials(getRootCertificateFromEnv(signalIdentifier)); + } + + return createSslCredentials(rootCertificate, clientKey, clientCertificate); +} + +function getCredentialsFromEnv(signalIdentifier: string): ChannelCredentials { + if (getInsecureSettingFromEnv(signalIdentifier)) { + return createInsecureCredentials(); + } + + return getCredentialsFromEnvIgnoreInsecure(signalIdentifier); +} + +export function getOtlpGrpcConfigurationFromEnv( + signalIdentifier: string +): Partial { + return { + ...getSharedConfigurationFromEnvironment(signalIdentifier), + metadata: getMetadataProviderFromEnv(signalIdentifier), + url: getUrlFromEnv(signalIdentifier), + credentials: (finalResolvedUrl: string) => { + // Always assume insecure on http:// and secure on https://, the protocol always takes precedence over the insecure setting. + // note: the spec does not make any exception for + // - "localhost:4317". If the protocol is omitted, credentials are required unless insecure is set + // - "unix://", as it's neither http:// nor https:// and therefore credentials are required unless insecure is set + if (finalResolvedUrl.startsWith('http://')) { + return () => { + return createInsecureCredentials(); + }; + } else if (finalResolvedUrl.startsWith('https://')) { + return () => { + return getCredentialsFromEnvIgnoreInsecure(signalIdentifier); + }; + } + + // defer to env settings in this case + return () => { + return getCredentialsFromEnv(signalIdentifier); + }; + }, + }; +} diff --git a/experimental/packages/otlp-grpc-exporter-base/src/index.ts b/experimental/packages/otlp-grpc-exporter-base/src/index.ts index 566b12f42f..3a445ed91b 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/index.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/index.ts @@ -16,4 +16,3 @@ export { OTLPGRPCExporterNodeBase } from './OTLPGRPCExporterNodeBase'; export { OTLPGRPCExporterConfigNode } from './types'; -export { DEFAULT_COLLECTOR_URL, validateAndNormalizeUrl } from './util'; diff --git a/experimental/packages/otlp-grpc-exporter-base/src/types.ts b/experimental/packages/otlp-grpc-exporter-base/src/types.ts index 43caad1371..61f5613de3 100644 --- a/experimental/packages/otlp-grpc-exporter-base/src/types.ts +++ b/experimental/packages/otlp-grpc-exporter-base/src/types.ts @@ -19,19 +19,8 @@ import type { ChannelCredentials, Metadata } from '@grpc/grpc-js'; import { CompressionAlgorithm, OTLPExporterConfigBase, - OTLPExporterError, } from '@opentelemetry/otlp-exporter-base'; -/** - * Queue item to be used to save temporary spans/metrics/logs in case the GRPC service - * hasn't been fully initialized yet - */ -export interface GRPCQueueItem { - objects: ExportedItem[]; - onSuccess: () => void; - onError: (error: OTLPExporterError) => void; -} - /** * OTLP Exporter Config for Node */ diff --git a/experimental/packages/otlp-grpc-exporter-base/src/util.ts b/experimental/packages/otlp-grpc-exporter-base/src/util.ts deleted file mode 100644 index 88a7cc37ec..0000000000 --- a/experimental/packages/otlp-grpc-exporter-base/src/util.ts +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 { getEnv } from '@opentelemetry/core'; -import * as path from 'path'; -import { URL } from 'url'; -import * as fs from 'fs'; -import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; -import { - createInsecureCredentials, - createSslCredentials, -} from './grpc-exporter-transport'; - -// NOTE: do not change these type imports to actual imports. Doing so WILL break `@opentelemetry/instrumentation-http`, -// as they'd be imported before the http/https modules can be wrapped. -import type { ChannelCredentials } from '@grpc/grpc-js'; - -export const DEFAULT_COLLECTOR_URL = 'http://localhost:4317'; - -export function validateAndNormalizeUrl(url: string): string { - const hasProtocol = url.match(/^([\w]{1,8}):\/\//); - if (!hasProtocol) { - url = `https://${url}`; - } - const target = new URL(url); - if (target.protocol === 'unix:') { - return url; - } - if (target.pathname && target.pathname !== '/') { - diag.warn( - 'URL path should not be set when using grpc, the path part of the URL will be ignored.' - ); - } - if (target.protocol !== '' && !target.protocol?.match(/^(http)s?:$/)) { - diag.warn('URL protocol should be http(s)://. Using http://.'); - } - return target.host; -} - -export function configureCredentials( - credentials: ChannelCredentials | undefined, - endpoint: string -): ChannelCredentials { - let insecure: boolean; - - if (credentials) { - return credentials; - } else if (endpoint.startsWith('https://')) { - insecure = false; - } else if ( - endpoint.startsWith('http://') || - endpoint === DEFAULT_COLLECTOR_URL - ) { - insecure = true; - } else { - insecure = getSecurityFromEnv(); - } - - if (insecure) { - return createInsecureCredentials(); - } else { - return getCredentialsFromEnvironment(); - } -} - -function getSecurityFromEnv(): boolean { - const definedInsecure = - getEnv().OTEL_EXPORTER_OTLP_TRACES_INSECURE || - getEnv().OTEL_EXPORTER_OTLP_INSECURE; - - if (definedInsecure) { - return definedInsecure.toLowerCase() === 'true'; - } else { - return false; - } -} - -/** - * Exported for testing - */ -export function getCredentialsFromEnvironment(): ChannelCredentials { - const rootCert = retrieveRootCert(); - const privateKey = retrievePrivateKey(); - const certChain = retrieveCertChain(); - - return createSslCredentials(rootCert, privateKey, certChain); -} - -function retrieveRootCert(): Buffer | undefined { - const rootCertificate = - getEnv().OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE || - getEnv().OTEL_EXPORTER_OTLP_CERTIFICATE; - - if (rootCertificate) { - try { - return fs.readFileSync(path.resolve(process.cwd(), rootCertificate)); - } catch { - diag.warn('Failed to read root certificate file'); - return undefined; - } - } else { - return undefined; - } -} - -function retrievePrivateKey(): Buffer | undefined { - const clientKey = - getEnv().OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY || - getEnv().OTEL_EXPORTER_OTLP_CLIENT_KEY; - - if (clientKey) { - try { - return fs.readFileSync(path.resolve(process.cwd(), clientKey)); - } catch { - diag.warn('Failed to read client certificate private key file'); - return undefined; - } - } else { - return undefined; - } -} - -function retrieveCertChain(): Buffer | undefined { - const clientChain = - getEnv().OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE || - getEnv().OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE; - - if (clientChain) { - try { - return fs.readFileSync(path.resolve(process.cwd(), clientChain)); - } catch { - diag.warn('Failed to read client certificate chain file'); - return undefined; - } - } else { - return undefined; - } -} - -export function configureCompression( - compression: CompressionAlgorithm | undefined -): CompressionAlgorithm { - if (compression != null) { - return compression; - } - - const envCompression = - getEnv().OTEL_EXPORTER_OTLP_TRACES_COMPRESSION || - getEnv().OTEL_EXPORTER_OTLP_COMPRESSION || - 'none'; - - if (envCompression === 'gzip') { - return CompressionAlgorithm.GZIP; - } else if (envCompression === 'none') { - return CompressionAlgorithm.NONE; - } - - diag.warn( - 'Unknown compression "' + envCompression + '", falling back to "none"' - ); - return CompressionAlgorithm.NONE; -} diff --git a/experimental/packages/otlp-grpc-exporter-base/test/OTLPGRPCExporterNodeBase.test.ts b/experimental/packages/otlp-grpc-exporter-base/test/OTLPGRPCExporterNodeBase.test.ts index 99677dd05a..037fc53895 100644 --- a/experimental/packages/otlp-grpc-exporter-base/test/OTLPGRPCExporterNodeBase.test.ts +++ b/experimental/packages/otlp-grpc-exporter-base/test/OTLPGRPCExporterNodeBase.test.ts @@ -17,7 +17,6 @@ import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; import { OTLPGRPCExporterNodeBase } from '../src/OTLPGRPCExporterNodeBase'; -import { OTLPGRPCExporterConfigNode } from '../src/types'; import { mockedReadableSpan } from './traceHelper'; import { ExportResponse, @@ -30,15 +29,7 @@ import sinon = require('sinon'); class MockCollectorExporter extends OTLPGRPCExporterNodeBase< ReadableSpan, any -> { - getDefaultUrl(config: OTLPGRPCExporterConfigNode): string { - return ''; - } - - getUrlFromConfig(config: OTLPGRPCExporterConfigNode): string { - return ''; - } -} +> {} const successfulResponse: ExportResponseSuccess = { status: 'success', @@ -55,9 +46,6 @@ describe('OTLPGRPCExporterNodeBase', () => { shutdown: sinon.stub(), }; const mockTransport = transportStubs; - const signalSpecificMetadata: Record = { - key: 'signal-specific-metadata', - }; const serializerStubs = { serializeRequest: sinon.stub().resolves(Buffer.from([1, 2, 3])), @@ -70,10 +58,10 @@ describe('OTLPGRPCExporterNodeBase', () => { exporter = new MockCollectorExporter( { concurrencyLimit }, - signalSpecificMetadata, + serializer, 'grpcName', 'grpcPath', - serializer + 'SIGNAL' ); exporter['_transport'] = mockTransport; diff --git a/experimental/packages/otlp-grpc-exporter-base/test/configuration/otlp-grpc-configuration.test.ts b/experimental/packages/otlp-grpc-exporter-base/test/configuration/otlp-grpc-configuration.test.ts new file mode 100644 index 0000000000..097612a18d --- /dev/null +++ b/experimental/packages/otlp-grpc-exporter-base/test/configuration/otlp-grpc-configuration.test.ts @@ -0,0 +1,259 @@ +/* + * 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 { + getOtlpGrpcDefaultConfiguration, + mergeOtlpGrpcConfigurationWithDefaults, +} from '../../src/configuration/otlp-grpc-configuration'; +import { + createEmptyMetadata, + createInsecureCredentials, + createSslCredentials, +} from '../../src/grpc-exporter-transport'; +import * as fs from 'fs'; + +describe('mergeOtlpGrpcConfigurationWithDefaults', function () { + describe('metadata', function () { + it('merges metadata instead of overriding', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { + metadata: () => { + const metadata = createEmptyMetadata(); + metadata.set('foo', 'foo-user'); + metadata.set('baz', 'baz-user'); + return metadata; + }, + }, + { + metadata: () => { + const metadata = createEmptyMetadata(); + metadata.set('foo', 'foo-fallback'); + metadata.set('bar', 'bar-fallback'); + return metadata; + }, + }, + getOtlpGrpcDefaultConfiguration() + ); + + // assert + assert.deepStrictEqual(config.metadata().getMap(), { + foo: 'foo-user', // does not use fallback if the user has set something + bar: 'bar-fallback', // uses fallback if there is no value set + baz: 'baz-user', // does not drop user-set metadata if there is no fallback for it + 'user-agent': 'OTel-OTLP-Exporter-JavaScript/0.53.0', + }); + }); + + it('does not override default (required) metadata', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { + metadata: () => { + const metadata = createEmptyMetadata(); + metadata.set('user-agent', 'user-provided-user-agent'); + return metadata; + }, + }, + { + metadata: () => { + const metadata = createEmptyMetadata(); + metadata.set('user-agent', 'fallback-user-agent'); + return metadata; + }, + }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.metadata().getMap(), { + 'user-agent': 'OTel-OTLP-Exporter-JavaScript/0.53.0', + }); + }); + + it('does use default metadata if nothing is provided', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + {}, + {}, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.metadata().getMap(), { + 'user-agent': 'OTel-OTLP-Exporter-JavaScript/0.53.0', + }); + }); + }); + + describe('url', function () { + it('uses user-provided url over fallback', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { url: 'http://user-provided.example.test:8000' }, + { url: 'http://fallback.example.test:8001' }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.url, 'user-provided.example.test:8000'); + }); + + it('should trim user-provided url', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { url: ' http://user-provided.example.test:8000 ' }, + { url: 'http://fallback.example.test:8001' }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.url, 'user-provided.example.test:8000'); + }); + + it('uses fallback url over default', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + {}, + { url: 'http://fallback.example.test:8001' }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.url, 'fallback.example.test:8001'); + }); + + it('should trim fallback url', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + {}, + { url: ' http://fallback.example.test:8001 ' }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.url, 'fallback.example.test:8001'); + }); + + it('should use default if nothing is provided', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + {}, + {}, + getOtlpGrpcDefaultConfiguration() + ); + + // assert + assert.deepStrictEqual(config.url, 'localhost:4317'); + }); + }); + + describe('credentials', function () { + it('uses user-provided credentials over fallback', function () { + // arrange + const userProvidedCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + Buffer.from(fs.readFileSync('./test/certs/client.key')), + Buffer.from(fs.readFileSync('./test/certs/client.crt')) + ); + + const fallbackCredentials = createInsecureCredentials(); + + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { + credentials: () => { + return userProvidedCredentials; + }, + }, + { + credentials: _url => { + return () => fallbackCredentials; + }, + }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.credentials(), userProvidedCredentials); + }); + + it('uses fallback credentials over default', function () { + // arrange + const fallbackCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + Buffer.from(fs.readFileSync('./test/certs/client.key')), + Buffer.from(fs.readFileSync('./test/certs/client.crt')) + ); + + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + {}, + { + credentials: _url => { + return () => fallbackCredentials; + }, + }, + getOtlpGrpcDefaultConfiguration() + ); + + assert.deepStrictEqual(config.credentials(), fallbackCredentials); + }); + + it('uses default (insecure) if nothing is provided', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + {}, + {}, + getOtlpGrpcDefaultConfiguration() + ); + + // assert + assert.deepStrictEqual(config.credentials(), createInsecureCredentials()); + }); + + it('uses default (secure) if https url is user-provided', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { url: 'https://user-provided.example.test:8000' }, + {}, + getOtlpGrpcDefaultConfiguration() + ); + + // assert + assert.deepStrictEqual(config.credentials(), createSslCredentials()); + }); + + it('uses default (insecure) if http url is user-provided', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { url: 'http://user-provided.example.test:8000' }, + {}, + getOtlpGrpcDefaultConfiguration() + ); + + // assert + assert.deepStrictEqual(config.credentials(), createInsecureCredentials()); + }); + + it('uses default (secure) if no protocol is provided', function () { + // act + const config = mergeOtlpGrpcConfigurationWithDefaults( + { url: 'user-provided.example.test:8000' }, + {}, + getOtlpGrpcDefaultConfiguration() + ); + + // assert + assert.deepStrictEqual(config.credentials(), createSslCredentials()); + }); + }); +}); diff --git a/experimental/packages/otlp-grpc-exporter-base/test/configuration/otlp-grpc-env-configuration.test.ts b/experimental/packages/otlp-grpc-exporter-base/test/configuration/otlp-grpc-env-configuration.test.ts new file mode 100644 index 0000000000..9c410a5fed --- /dev/null +++ b/experimental/packages/otlp-grpc-exporter-base/test/configuration/otlp-grpc-env-configuration.test.ts @@ -0,0 +1,488 @@ +/* + * 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 fs from 'fs'; +import * as sinon from 'sinon'; + +import { getOtlpGrpcConfigurationFromEnv } from '../../src/configuration/otlp-grpc-env-configuration'; +import { + createInsecureCredentials, + createSslCredentials, +} from '../../src/grpc-exporter-transport'; +import { diag } from '@opentelemetry/api'; + +describe('getOtlpGrpcConfigurationFromEnvironment', function () { + describe('metadata', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_HEADERS; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS; + }); + + it('remains unset if env vars are not set', function () { + // ensure both are not set + delete process.env.OTEL_EXPORTER_OTLP_HEADERS; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.metadata, undefined); + }); + + it('remains unset if env vars are set to empty string', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = ''; + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.metadata, undefined); + }); + + it('remains unset if non-specific env var is set to empty string, specific is undefined', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = ''; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.metadata, undefined); + }); + + it('remains unset if non-specific env var is a list of empty strings, specific is undefined', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = ', , , '; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.metadata, undefined); + }); + + it('remains unset if specific env var is set to empty string, non-specific is undefined', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ''; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.metadata, undefined); + }); + + it('remains unset if specific env var is a list of empty strings, non-specific is undefined', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = ', , , '; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.metadata, undefined); + }); + + it('merges metadata instead of overriding', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = + 'foo=foo-non-specific,bar=bar-non-specific'; + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'foo=foo-specific,baz=baz-specific'; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.deepEqual(config.metadata?.().getMap(), { + foo: 'foo-specific', // does not use specific if the user has set something + bar: 'bar-non-specific', // uses non-specific if there is nothing specific set + baz: 'baz-specific', // does not drop user-set metadata if there is no non-specific key for it + }); + }); + + it('allows non-specific only metadata', function () { + process.env.OTEL_EXPORTER_OTLP_HEADERS = + 'foo=foo-non-specific,bar=bar-non-specific'; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.deepEqual(config.metadata?.().getMap(), { + foo: 'foo-non-specific', + bar: 'bar-non-specific', + }); + }); + + it('allows specific only metadata', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_HEADERS = + 'foo=foo-specific,baz=baz-specific'; + + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.deepEqual(config.metadata?.().getMap(), { + foo: 'foo-specific', + baz: 'baz-specific', + }); + }); + }); + + describe('url', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT; + sinon.restore(); + }); + + it('should override non-signal specific exporter url with specific url', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://example.test/'; + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = + 'http://metrics.example.test/'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual( + config.url, + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + ); + }); + + it('should use non-specific url defined in env', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://example.test/'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, process.env.OTEL_EXPORTER_OTLP_ENDPOINT); + }); + + it('should use specific url defined in env', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'http://example.test/'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual( + config.url, + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + ); + }); + + it('should keep non-specific url as-is when no protocol is given', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'example.test'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, process.env.OTEL_EXPORTER_OTLP_ENDPOINT); + }); + + it('should keep specific url as-is when no protocol is given', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'example.test'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual( + config.url, + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + ); + }); + + it('should not drop any protocol that is unknown with non-specific url', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'foo://example.test'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, process.env.OTEL_EXPORTER_OTLP_ENDPOINT); + }); + + it('should not drop any protocol that is unknown with specific url', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'foo://example.test'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual( + config.url, + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + ); + }); + + it('should keep https protocol with non-specific url', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'https://example.test'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, process.env.OTEL_EXPORTER_OTLP_ENDPOINT); + }); + + it('should keep https protocol with specific url', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = 'https://example.test'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual( + config.url, + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + ); + }); + + it('should trim non-specific url', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ' http://example.test:4317 '; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, 'http://example.test:4317'); + }); + + it('should trim specific url', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = + ' http://example.test:4317 '; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, 'http://example.test:4317'); + }); + + it('should treat empty non-specific url as undefined', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ''; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, undefined); + }); + + it('should treat empty specific url as undefined', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ''; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, undefined); + }); + + it('should treat space-only non-specific url as undefined', function () { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = ' '; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, undefined); + }); + + it('should treat space-only specific url as undefined', function () { + process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT = ' '; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + assert.strictEqual(config.url, undefined); + }); + }); + + describe('credentials', function () { + afterEach(function () { + delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT; + delete process.env.OTEL_EXPORTER_OTLP_INSECURE; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE; + delete process.env.OTEL_EXPORTER_OTLP_INSECURE; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE; + delete process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE; + delete process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY; + delete process.env.OTEL_EXPORTER_OTLP_CERTIFICATE; + delete process.env.OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE; + sinon.restore(); + }); + + it('should select insecure credentials on http protocol', function () { + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('http://example.test:4317')(); + + assert.deepStrictEqual(credentials, createInsecureCredentials()); + }); + + it('should select secure credentials on https protocol', function () { + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('https://example.test:4317')(); + + assert.deepStrictEqual(credentials, createSslCredentials()); + }); + + it('should select secure credentials on https protocol even when insecure env var is set to true', function () { + // From the spec: + // "Insecure: This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme" + + // arrange + const expectedCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + Buffer.from(fs.readFileSync('./test/certs/client.key')), + Buffer.from(fs.readFileSync('./test/certs/client.crt')) + ); + process.env.OTEL_EXPORTER_OTLP_INSECURE = 'true'; + process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'true'; + process.env.OTEL_EXPORTER_OTLP_CERTIFICATE = './test/certs/ca.crt'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY = './test/certs/client.key'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = + './test/certs/client.crt'; + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('https://example.test:4317')(); + + // assert + assert.deepStrictEqual(credentials, expectedCredentials); + }); + + it('should select insecure credentials on http protocol even when insecure env var is set to false', function () { + // From the spec: + // "Insecure: This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme" + + process.env.OTEL_EXPORTER_OTLP_INSECURE = 'false'; + process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'false'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('http://example.test:4317')(); + + assert.deepStrictEqual(credentials, createInsecureCredentials()); + }); + + it('should select insecure credentials on http protocol even when insecure env var is set to true', function () { + // From the spec: + // "Insecure: This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme" + + process.env.OTEL_EXPORTER_OTLP_INSECURE = 'true'; + process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'true'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + assert.deepStrictEqual(credentials, createInsecureCredentials()); + }); + + it('should select insecure credentials on no protocol when insecure env var is set to true', function () { + // From the spec: + // "Insecure: This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme" + + process.env.OTEL_EXPORTER_OTLP_INSECURE = 'true'; + process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'true'; + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + assert.deepStrictEqual(credentials, createInsecureCredentials()); + }); + + it('should select non-specific secure credentials on no protocol when insecure env var is set to false', function () { + // From the spec: + // "Insecure: This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme" + + // arrange + const expectedCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + Buffer.from(fs.readFileSync('./test/certs/client.key')), + Buffer.from(fs.readFileSync('./test/certs/client.crt')) + ); + process.env.OTEL_EXPORTER_OTLP_INSECURE = 'false'; + process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'false'; + process.env.OTEL_EXPORTER_OTLP_CERTIFICATE = './test/certs/ca.crt'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY = './test/certs/client.key'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = + './test/certs/client.crt'; + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + // assert + assert.deepStrictEqual(credentials, expectedCredentials); + }); + + it('should select specific secure credentials on no protocol when insecure env var is set to false', function () { + // From the spec: + // "Insecure: This option only applies to OTLP/gRPC when an endpoint is provided without the http or https scheme" + + // arrange + const expectedCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + Buffer.from(fs.readFileSync('./test/certs/client.key')), + Buffer.from(fs.readFileSync('./test/certs/client.crt')) + ); + process.env.OTEL_EXPORTER_OTLP_METRICS_INSECURE = 'false'; + process.env.OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE = + './test/certs/ca.crt'; + process.env.OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY = + './test/certs/client.key'; + process.env.OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE = + './test/certs/client.crt'; + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + // assert + assert.deepStrictEqual(credentials, expectedCredentials); + }); + + it('should warn when client key is missing but client cert is there', function () { + // arrange + const spyLoggerWarn = sinon.stub(diag, 'warn'); + const expectedCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + undefined, + undefined + ); + + process.env.OTEL_EXPORTER_OTLP_CERTIFICATE = './test/certs/ca.crt'; + // OTEL_EXPORTER_OTLP_CLIENT_KEY not set + process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = + './test/certs/client.crt'; + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + // assert + sinon.assert.calledOnceWithExactly( + spyLoggerWarn, + 'Client key and certificate must both be provided, but one was missing - attempting to create credentials from just the root certificate' + ); + assert.deepStrictEqual(credentials, expectedCredentials); + }); + + it('should warn when client cert is missing but client key is there', function () { + // arrange + const spyLoggerWarn = sinon.stub(diag, 'warn'); + const expectedCredentials = createSslCredentials( + Buffer.from(fs.readFileSync('./test/certs/ca.crt')), + undefined, + undefined + ); + + process.env.OTEL_EXPORTER_OTLP_CERTIFICATE = './test/certs/ca.crt'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY = './test/certs/client.key'; + // OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE not set + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + // assert + sinon.assert.calledOnceWithExactly( + spyLoggerWarn, + 'Client key and certificate must both be provided, but one was missing - attempting to create credentials from just the root certificate' + ); + assert.deepStrictEqual(credentials, expectedCredentials); + }); + + it('should not warn when root certificate is missing', function () { + // arrange + const spyLoggerWarn = sinon.stub(diag, 'warn'); + const expectedCredentials = createSslCredentials( + undefined, + Buffer.from(fs.readFileSync('./test/certs/client.key')), + Buffer.from(fs.readFileSync('./test/certs/client.crt')) + ); + + // OTEL_EXPORTER_OTLP_CERTIFICATE not set + process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY = './test/certs/client.key'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = + './test/certs/client.crt'; + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + // assert + sinon.assert.notCalled(spyLoggerWarn); + assert.deepStrictEqual(credentials, expectedCredentials); + }); + + it('should warn when files are not accessible', function () { + // arrange + const spyLoggerWarn = sinon.stub(diag, 'warn'); + const expectedCredentials = createSslCredentials(); + + // OTEL_EXPORTER_OTLP_CERTIFICATE not set + process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY = './test/certs/client.key'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = + './test/certs/client.crt'; + + process.env.OTEL_EXPORTER_OTLP_CERTIFICATE = + './test/certs/non-existent-ca.crt'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_KEY = + './test/certs/non-existent-client.key'; + process.env.OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE = + './test/certs/non-existent-client.crt'; + + // act + const config = getOtlpGrpcConfigurationFromEnv('METRICS'); + const credentials = config.credentials?.('example.test:4317')(); + + // assert + sinon.assert.callCount(spyLoggerWarn, 3); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Failed to read root certificate file' + ); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Failed to read client certificate private key file' + ); + sinon.assert.calledWithExactly( + spyLoggerWarn, + 'Failed to read client certificate chain file' + ); + assert.deepStrictEqual(credentials, expectedCredentials); + }); + }); +}); diff --git a/experimental/packages/otlp-grpc-exporter-base/test/otlp-grpc-configuration.test.ts b/experimental/packages/otlp-grpc-exporter-base/test/otlp-grpc-configuration.test.ts new file mode 100644 index 0000000000..85652af5f8 --- /dev/null +++ b/experimental/packages/otlp-grpc-exporter-base/test/otlp-grpc-configuration.test.ts @@ -0,0 +1,83 @@ +/* + * 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 * as sinon from 'sinon'; +import * as assert from 'assert'; +import { validateAndNormalizeUrl } from '../src/configuration/otlp-grpc-configuration'; + +describe('validateAndNormalizeUrl()', function () { + const tests = [ + { + name: 'bare hostname should return same value', + input: 'api.datacat.io', + expected: 'api.datacat.io', + }, + { + name: 'host:port should return same value', + input: 'api.datacat.io:1234', + expected: 'api.datacat.io:1234', + }, + { + name: 'https://host:port should trim off protocol', + input: 'https://api.datacat.io:1234', + expected: 'api.datacat.io:1234', + }, + { + name: 'should accept unix socket', + input: 'unix:///tmp/grpc.sock', + expected: 'unix:///tmp/grpc.sock', + }, + { + name: 'bad protocol should warn but return host:port', + input: 'badproto://api.datacat.io:1234', + expected: 'api.datacat.io:1234', + warn: 'URL protocol should be http(s)://. Using http://.', + }, + { + name: 'path on end of url should warn but return host:port', + input: 'http://api.datacat.io:1234/a/b/c', + expected: 'api.datacat.io:1234', + warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', + }, + { + name: ':// in path should not be used for protocol even if protocol not specified', + input: 'api.datacat.io/a/b://c', + expected: 'api.datacat.io', + warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', + }, + { + name: ':// in path is valid when a protocol is specified', + input: 'http://api.datacat.io/a/b://c', + expected: 'api.datacat.io', + warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', + }, + ]; + tests.forEach(test => { + it(test.name, function () { + const diagWarn = sinon.stub(diag, 'warn'); + try { + assert.strictEqual(validateAndNormalizeUrl(test.input), test.expected); + if (test.warn) { + sinon.assert.calledWith(diagWarn, test.warn); + } else { + sinon.assert.notCalled(diagWarn); + } + } finally { + diagWarn.restore(); + } + }); + }); +}); diff --git a/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts b/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts deleted file mode 100644 index 1da067b559..0000000000 --- a/experimental/packages/otlp-grpc-exporter-base/test/util.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -/* - * 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 sinon from 'sinon'; -import * as assert from 'assert'; - -import { diag } from '@opentelemetry/api'; -import * as grpc from '@grpc/grpc-js'; -import { - validateAndNormalizeUrl, - configureCompression, - configureCredentials, - getCredentialsFromEnvironment, - DEFAULT_COLLECTOR_URL, -} from '../src/util'; -import { CompressionAlgorithm } from '@opentelemetry/otlp-exporter-base'; - -// Tests added to detect breakage released in #2130 -describe('validateAndNormalizeUrl()', () => { - const tests = [ - { - name: 'bare hostname should return same value', - input: 'api.datacat.io', - expected: 'api.datacat.io', - }, - { - name: 'host:port should return same value', - input: 'api.datacat.io:1234', - expected: 'api.datacat.io:1234', - }, - { - name: 'https://host:port should trim off protocol', - input: 'https://api.datacat.io:1234', - expected: 'api.datacat.io:1234', - }, - { - name: 'should accept unix socket', - input: 'unix:///tmp/grpc.sock', - expected: 'unix:///tmp/grpc.sock', - }, - { - name: 'bad protocol should warn but return host:port', - input: 'badproto://api.datacat.io:1234', - expected: 'api.datacat.io:1234', - warn: 'URL protocol should be http(s)://. Using http://.', - }, - { - name: 'path on end of url should warn but return host:port', - input: 'http://api.datacat.io:1234/a/b/c', - expected: 'api.datacat.io:1234', - warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', - }, - { - name: ':// in path should not be used for protocol even if protocol not specified', - input: 'api.datacat.io/a/b://c', - expected: 'api.datacat.io', - warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', - }, - { - name: ':// in path is valid when a protocol is specified', - input: 'http://api.datacat.io/a/b://c', - expected: 'api.datacat.io', - warn: 'URL path should not be set when using grpc, the path part of the URL will be ignored.', - }, - ]; - tests.forEach(test => { - it(test.name, () => { - const diagWarn = sinon.stub(diag, 'warn'); - try { - assert.strictEqual(validateAndNormalizeUrl(test.input), test.expected); - if (test.warn) { - sinon.assert.calledWith(diagWarn, test.warn); - } else { - sinon.assert.notCalled(diagWarn); - } - } finally { - diagWarn.restore(); - } - }); - }); -}); - -describe('utils - configureCredentials', () => { - const envSource = process.env; - it('should return insecure channel when using all defaults', () => { - const credentials = configureCredentials(undefined, DEFAULT_COLLECTOR_URL); - assert.ok(credentials._isSecure() === false); - }); - it('should return user defined channel credentials', () => { - const userDefinedCredentials = grpc.credentials.createSsl(); - const credentials = configureCredentials( - userDefinedCredentials, - 'http://foo.bar' - ); - - assert.ok(userDefinedCredentials === credentials); - assert.ok(credentials._isSecure() === true); - }); - it('should return secure channel when endpoint contains https scheme - no matter insecure env settings,', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE = 'true'; - const credentials = configureCredentials(undefined, 'https://foo.bar'); - assert.ok(credentials._isSecure() === true); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE; - }); - - it('should return insecure channel when endpoint contains http scheme and no insecure env settings', () => { - const credentials = configureCredentials(undefined, 'http://foo.bar'); - assert.ok(credentials._isSecure() === false); - }); - it('should return secure channel when endpoint does not contain scheme and no insecure env settings', () => { - const credentials = configureCredentials(undefined, 'foo.bar'); - assert.ok(credentials._isSecure() === true); - }); - it('should return insecure channel when endpoint contains http scheme and insecure env set to false', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE = 'false'; - const credentials = configureCredentials(undefined, 'http://foo.bar'); - assert.ok(credentials._isSecure() === false); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE; - }); - it('should return insecure channel when endpoint contains http scheme and insecure env set to true', () => { - envSource.OTEL_EXPORTER_OTLP_INSECURE = 'true'; - const credentials = configureCredentials(undefined, 'http://localhost'); - assert.ok(credentials._isSecure() === false); - delete envSource.OTEL_EXPORTER_OTLP_INSECURE; - }); - it('should return secure channel when endpoint does not contain scheme and insecure env set to false', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE = 'false'; - const credentials = configureCredentials(undefined, 'foo.bar'); - assert.ok(credentials._isSecure() === true); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_INSECURE; - }); - it('should return insecure channel when endpoint does not contain scheme and insecure env set to true', () => { - envSource.OTEL_EXPORTER_OTLP_INSECURE = 'true'; - const credentials = configureCredentials(undefined, 'foo.bar'); - assert.ok(credentials._isSecure() === false); - delete envSource.OTEL_EXPORTER_OTLP_INSECURE; - }); -}); - -describe('useSecureConnection', () => { - const envSource = process.env; - it('should return secure connection using all credentials', () => { - envSource.OTEL_EXPORTER_OTLP_CERTIFICATE = './test/certs/ca.crt'; - envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY = './test/certs/client.key'; - envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE = - './test/certs/client.crt'; - - const credentials = getCredentialsFromEnvironment(); - assert.ok(credentials._isSecure() === true); - - delete envSource.OTEL_EXPORTER_OTLP_CERTIFICATE; - delete envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_KEY; - delete envSource.OTEL_EXPORTER_OTLP_TRACES_CLIENT_CERTIFICATE; - }); - it('should return secure connection using only root certificate', () => { - envSource.OTEL_EXPORTER_OTLP_CERTIFICATE = './test/certs/ca.crt'; - const credentials = getCredentialsFromEnvironment(); - assert.ok(credentials._isSecure() === true); - delete envSource.OTEL_EXPORTER_OTLP_CERTIFICATE; - }); - it('should warn user when file cannot be read and use default root certificate', () => { - envSource.OTEL_EXPORTER_OTLP_CERTIFICATE = './wrongpath/test/certs/ca.crt'; - const diagWarn = sinon.stub(diag, 'warn'); - const credentials = getCredentialsFromEnvironment(); - const args = diagWarn.args[0]; - - assert.strictEqual(args[0], 'Failed to read root certificate file'); - sinon.assert.calledOnce(diagWarn); - assert.ok(credentials._isSecure() === true); - - delete envSource.OTEL_EXPORTER_OTLP_CERTIFICATE; - diagWarn.restore(); - }); -}); - -describe('configureCompression', () => { - const envSource = process.env; - it('should return none for compression', () => { - const compression = CompressionAlgorithm.NONE; - assert.strictEqual( - configureCompression(compression), - CompressionAlgorithm.NONE - ); - }); - it('should return gzip compression defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = 'gzip'; - assert.strictEqual( - configureCompression(undefined), - CompressionAlgorithm.GZIP - ); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION; - }); - it('should return none for compression defined via env', () => { - envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION = 'none'; - assert.strictEqual( - configureCompression(undefined), - CompressionAlgorithm.NONE - ); - delete envSource.OTEL_EXPORTER_OTLP_TRACES_COMPRESSION; - }); - it('should return none for compression when no compression is set', () => { - assert.strictEqual( - configureCompression(undefined), - CompressionAlgorithm.NONE - ); - }); -});