diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index f5dbfd2a8f..4935d9b83b 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to experimental packages in this project will be documented * feat(otlp-trace-exporters): Add User-Agent header to OTLP trace exporters. [#3790](https://github.com/open-telemetry/opentelemetry-js/pull/3790) @JamieDanielson * feat(otlp-metric-exporters): Add User-Agent header to OTLP metric exporters. [#3806](https://github.com/open-telemetry/opentelemetry-js/pull/3806) @JamieDanielson * feat(opencensus-shim): add OpenCensus trace shim [#3809](https://github.com/open-telemetry/opentelemetry-js/pull/3809) @aabmass +* feat(exporter-logs-otlp-proto): protobuf exporter for logs. [#3779](https://github.com/open-telemetry/opentelemetry-js/pull/3779) @Abinet18 ### :bug: (Bug Fix) diff --git a/experimental/packages/exporter-logs-otlp-proto/.eslintignore b/experimental/packages/exporter-logs-otlp-proto/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/.eslintignore @@ -0,0 +1 @@ +build diff --git a/experimental/packages/exporter-logs-otlp-proto/.eslintrc.js b/experimental/packages/exporter-logs-otlp-proto/.eslintrc.js new file mode 100644 index 0000000000..3ed0fbeba3 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + "env": { + "mocha": true, + "commonjs": true, + "node": true, + }, + ...require('../../../eslint.config.js') +} diff --git a/experimental/packages/exporter-logs-otlp-proto/.npmignore b/experimental/packages/exporter-logs-otlp-proto/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/experimental/packages/exporter-logs-otlp-proto/LICENSE b/experimental/packages/exporter-logs-otlp-proto/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. diff --git a/experimental/packages/exporter-logs-otlp-proto/README.md b/experimental/packages/exporter-logs-otlp-proto/README.md new file mode 100644 index 0000000000..3019f50d3f --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/README.md @@ -0,0 +1,110 @@ +# An OTLP exporter to send logs using protobuf over HTTP + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +**Note: This is an experimental package under active development. New releases may include breaking changes.** + +This module provides an exporter for OTLP (http/protobuf) logs using protocol version `v0.18`. + +## Installation + +```bash +npm install --save @opentelemetry/exporter-logs-otlp-proto +``` + +## Further Documentation + +To see documentation and sample code for the traces exporter, as well as instructions for using TLS, see the [exporter-trace-otlp-proto package][trace-exporter-url]. +To see documentation and sample code for the metric exporter, see the [exporter-trace-otlp-proto package][metrics-exporter-url]. + +## Example Setup + +```js +const { LoggerProvider, SimpleLogRecordProcessor } = require('@opentelemetry/sdk-logs'); +const { OTLPLogsExporter } = require('@opentelemetry/exporter-logs-otlp-proto'); + +const collectorOptions = { + url: '', // url is optional and can be omitted - default is http://localhost:4318/v1/logs + headers: { + foo: 'bar' + }, //an optional object containing custom headers to be sent with each request will only work with http +}; + +const logProvider = new LoggerProvider({resource: new Resource({'service.name': 'testApp'})}); +const logExporter = new OTLPLogsExporter(collectorOptions); +logProvider.addLogRecordProcessor(new SimpleLogRecordProcessor(exporter)); + +const logger = logProvider.getLogger('test_log_instrumentation'); + +logger.emit({ + //log data to emit +}) +``` + +## Exporter Timeout Configuration + +The OTLPLogsExporter has a timeout configuration option which is the maximum time, in milliseconds, the OTLP exporter will wait for each batch export. The default value is 10000ms. + +To override the default timeout duration, use the following options: + ++ Set with environment variables: + + | Environment variable | Description | +------------------------------|----------------------|-------------| + | OTEL_EXPORTER_OTLP_LOGS_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace batch. Default is 10000. | + | OTEL_EXPORTER_OTLP_TIMEOUT | The maximum waiting time, in milliseconds, allowed to send each OTLP trace and metric batch. Default is 10000. | + + > `OTEL_EXPORTER_OTLP_LOGS_TIMEOUT` takes precedence and overrides `OTEL_EXPORTER_OTLP_TIMEOUT`. + ++ Provide `timeoutMillis` to OTLPLogsExporter with `collectorOptions`: + + ```js + const collectorOptions = { + timeoutMillis: 15000, + url: '', // url is optional and can be omitted - default is http://localhost:4318/v1/logs + headers: { + foo: 'bar' + }, //an optional object containing custom headers to be sent with each request will only work with http + }; + + const exporter = new OTLPLogsExporter(collectorOptions); + ``` + + > Providing `timeoutMillis` with `collectorOptions` takes precedence and overrides timeout set with environment variables. + +## OTLP Exporter Retry + +OTLP requires that transient errors be handled with a [retry strategy](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#retry). + +This retry policy has the following configuration, which there is currently no way to customize. + ++ `DEFAULT_EXPORT_MAX_ATTEMPTS`: The maximum number of attempts, including the original request. Defaults to 5. ++ `DEFAULT_EXPORT_INITIAL_BACKOFF`: The initial backoff duration. Defaults to 1 second. ++ `DEFAULT_EXPORT_MAX_BACKOFF`: The maximum backoff duration. Defaults to 5 seconds. ++ `DEFAULT_EXPORT_BACKOFF_MULTIPLIER`: The backoff multiplier. Defaults to 1.5. + +This retry policy first checks if the response has a `'Retry-After'` header. If there is a `'Retry-After'` header, the exporter will wait the amount specified in the `'Retry-After'` header before retrying. If there is no `'Retry-After'` header, the exporter will use an exponential backoff with jitter retry strategy. + + > The exporter will retry exporting within the [exporter timeout configuration](#Exporter-Timeout-Configuration) time. + +## Useful links + ++ For more information on OpenTelemetry, visit: ++ For more about OpenTelemetry JavaScript: ++ For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/exporter-logs-otlp-proto +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fexporter-logs-otlp-proto.svg +[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector +[semconv-resource-service-name]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/README.md#service +[logs-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-logs-otlp-proto +[trace-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/exporter-trace-otlp-proto +[metrics-exporter-url]: https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-metrics-otlp-proto diff --git a/experimental/packages/exporter-logs-otlp-proto/karma.conf.js b/experimental/packages/exporter-logs-otlp-proto/karma.conf.js new file mode 100644 index 0000000000..4c60b54edb --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/karma.conf.js @@ -0,0 +1,26 @@ +/*! + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ + +const karmaWebpackConfig = require('../../../karma.webpack'); +const karmaBaseConfig = require('../../../karma.base'); + +module.exports = (config) => { + config.set(Object.assign({}, karmaBaseConfig, { + webpack: karmaWebpackConfig, + files: ['test/browser/index-webpack.ts'], + preprocessors: { 'test/browser/index-webpack.ts': ['webpack'] } + })) +}; diff --git a/experimental/packages/exporter-logs-otlp-proto/package.json b/experimental/packages/exporter-logs-otlp-proto/package.json new file mode 100644 index 0000000000..016ec13486 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/package.json @@ -0,0 +1,95 @@ +{ + "name": "@opentelemetry/exporter-logs-otlp-proto", + "version": "0.39.1", + "description": "An OTLP exporter to send logs using protobuf over HTTP", + "main": "build/src/index.js", + "module": "build/esm/index.js", + "esnext": "build/esnext/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "browser": { + "./src/platform/index.ts": "./src/platform/browser/index.ts", + "./build/esm/platform/index.js": "./build/esm/platform/browser/index.js", + "./build/esnext/platform/index.js": "./build/esnext/platform/browser/index.js", + "./build/src/platform/index.js": "./build/src/platform/browser/index.js" + }, + "scripts": { + "prepublishOnly": "npm run compile", + "compile": "tsc --build tsconfig.json tsconfig.esm.json tsconfig.esnext.json", + "clean": "tsc --build --clean tsconfig.json tsconfig.esm.json tsconfig.esnext.json", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'", + "test:browser": "nyc karma start --single-run", + "version": "node ../../../scripts/version-update.js", + "watch": "tsc --build --watch tsconfig.json tsconfig.esm.json tsconfig.esnext.json", + "precompile": "lerna run version --scope $(npm pkg get name) --include-dependencies", + "prewatch": "npm run precompile", + "peer-api-check": "node ../../../scripts/peer-api-check.js", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "protobuf", + "tracing", + "profiling", + "metrics", + "stats", + "logs" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "files": [ + "build/esm/**/*.js", + "build/esm/**/*.js.map", + "build/esm/**/*.d.ts", + "build/esnext/**/*.js", + "build/esnext/**/*.js.map", + "build/esnext/**/*.d.ts", + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts", + "build/protos/**/*.proto", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@babel/core": "7.16.0", + "@opentelemetry/api": "1.4.1", + "@types/mocha": "10.0.0", + "@types/node": "18.6.5", + "@types/sinon": "10.0.13", + "codecov": "3.8.3", + "cpx": "1.5.0", + "mocha": "10.0.0", + "nyc": "15.1.0", + "sinon": "15.0.0", + "ts-loader": "8.4.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + }, + "dependencies": { + "@opentelemetry/core": "1.13.0", + "@opentelemetry/otlp-transformer": "0.39.1", + "@opentelemetry/resources": "1.13.0", + "@opentelemetry/sdk-trace-base": "1.13.0", + "@opentelemetry/api-logs": "^0.39.1", + "@opentelemetry/otlp-exporter-base": "0.39.1", + "@opentelemetry/otlp-proto-exporter-base": "0.39.1", + "@opentelemetry/sdk-logs": "^0.39.1" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/exporter-logs-otlp-proto", + "sideEffects": false +} diff --git a/experimental/packages/exporter-logs-otlp-proto/src/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/index.ts new file mode 100644 index 0000000000..9fde4be45b --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/src/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { OTLPLogsExporter } from './platform'; diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts new file mode 100644 index 0000000000..185d5094f1 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/OTLPLogsExporter.ts @@ -0,0 +1,76 @@ +/* + * 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, baggageUtils } from '@opentelemetry/core'; +import { + OTLPExporterConfigBase, + appendResourcePathToUrl, + appendRootPathToUrlIfNeeded, +} from '@opentelemetry/otlp-exporter-base'; +import { + OTLPProtoExporterBrowserBase, + ServiceClientType, +} from '@opentelemetry/otlp-proto-exporter-base'; +import { + createExportLogsServiceRequest, + IExportLogsServiceRequest, +} from '@opentelemetry/otlp-transformer'; + +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 + */ +export class OTLPLogsExporter + extends OTLPProtoExporterBrowserBase< + ReadableLogRecord, + IExportLogsServiceRequest + > + implements LogRecordExporter +{ + constructor(config: OTLPExporterConfigBase = {}) { + super(config); + this._headers = Object.assign( + this._headers, + baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS + ) + ); + } + convert(logs: ReadableLogRecord[]): IExportLogsServiceRequest { + return createExportLogsServiceRequest(logs); + } + + getDefaultUrl(config: OTLPExporterConfigBase): string { + return typeof config.url === 'string' + ? config.url + : getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.length > 0 + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) + : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 + ? appendResourcePathToUrl( + getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, + DEFAULT_COLLECTOR_RESOURCE_PATH + ) + : DEFAULT_COLLECTOR_URL; + } + + getServiceClientType() { + return ServiceClientType.LOGS; + } +} diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts new file mode 100644 index 0000000000..e6968b3466 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/browser/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { OTLPLogsExporter } from './OTLPLogsExporter'; diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts new file mode 100644 index 0000000000..851ff9a015 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export { OTLPLogsExporter } from './node'; diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts new file mode 100644 index 0000000000..88d97e21d3 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/OTLPLogsExporter.ts @@ -0,0 +1,76 @@ +/* + * 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, baggageUtils } from '@opentelemetry/core'; +import { + OTLPExporterConfigBase, + appendResourcePathToUrl, + appendRootPathToUrlIfNeeded, +} from '@opentelemetry/otlp-exporter-base'; +import { + OTLPProtoExporterNodeBase, + ServiceClientType, +} from '@opentelemetry/otlp-proto-exporter-base'; +import { + createExportLogsServiceRequest, + IExportLogsServiceRequest, +} from '@opentelemetry/otlp-transformer'; + +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 Node + */ +export class OTLPLogsExporter + extends OTLPProtoExporterNodeBase< + ReadableLogRecord, + IExportLogsServiceRequest + > + implements LogRecordExporter +{ + constructor(config: OTLPExporterConfigBase = {}) { + super(config); + this.headers = Object.assign( + this.headers, + baggageUtils.parseKeyPairsIntoRecord( + getEnv().OTEL_EXPORTER_OTLP_LOGS_HEADERS + ) + ); + } + convert(logs: ReadableLogRecord[]): IExportLogsServiceRequest { + return createExportLogsServiceRequest(logs); + } + + getDefaultUrl(config: OTLPExporterConfigBase): string { + return typeof config.url === 'string' + ? config.url + : getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.length > 0 + ? appendRootPathToUrlIfNeeded(getEnv().OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) + : getEnv().OTEL_EXPORTER_OTLP_ENDPOINT.length > 0 + ? appendResourcePathToUrl( + getEnv().OTEL_EXPORTER_OTLP_ENDPOINT, + DEFAULT_COLLECTOR_RESOURCE_PATH + ) + : DEFAULT_COLLECTOR_URL; + } + + getServiceClientType() { + return ServiceClientType.LOGS; + } +} diff --git a/experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts new file mode 100644 index 0000000000..4797b30651 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/src/platform/node/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { OTLPLogsExporter } from './OTLPLogsExporter'; diff --git a/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts new file mode 100644 index 0000000000..382a132c3e --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/test/browser/OTLPLogsExporter.test.ts @@ -0,0 +1,50 @@ +/* + * 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 { OTLPLogsExporter } from '../../src/platform/browser/index'; + +describe('OTLPLogsExporter - web', () => { + let collectorLogsExporter: OTLPLogsExporter; + describe('constructor', () => { + let onInitSpy: any; + beforeEach(() => { + onInitSpy = sinon.stub(OTLPLogsExporter.prototype, 'onInit'); + const collectorExporterConfig = { + hostname: 'foo', + url: 'http://foo.bar.com', + }; + collectorLogsExporter = new OTLPLogsExporter(collectorExporterConfig); + }); + afterEach(() => { + sinon.restore(); + }); + 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/browser/index-webpack.ts b/experimental/packages/exporter-logs-otlp-proto/test/browser/index-webpack.ts new file mode 100644 index 0000000000..ae7d4b5a9d --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/test/browser/index-webpack.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ +const testsContext = require.context('../browser', true, /test$/); +testsContext.keys().forEach(testsContext); + +const srcContext = require.context('.', true, /src$/); +srcContext.keys().forEach(srcContext); diff --git a/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts b/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts new file mode 100644 index 0000000000..7b8c8e8b9a --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/test/logHelper.ts @@ -0,0 +1,186 @@ +/* + * 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 { HrTime, TraceFlags } from '@opentelemetry/api'; +import { SeverityNumber } from '@opentelemetry/api-logs'; +import { Resource } from '@opentelemetry/resources'; +import * as assert from 'assert'; +import { VERSION } from '@opentelemetry/core'; +import { + IAnyValue, + IExportLogsServiceRequest, + IKeyValue, + ILogRecord, + IResource, +} from '@opentelemetry/otlp-transformer'; +import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; +import { Stream } from 'stream'; + +export const mockedReadableLogRecord: ReadableLogRecord = { + resource: Resource.default().merge( + new Resource({ + 'resource-attribute': 'some resource-attr value', + }) + ), + instrumentationScope: { + name: 'scope_name_1', + version: '0.1.0', + schemaUrl: 'http://url.to.schema', + }, + hrTime: [1680253513, 123241635] as HrTime, + hrTimeObserved: [1680253513, 123241635] as HrTime, + attributes: { + 'some-attribute': 'some attribute value', + }, + severityNumber: SeverityNumber.ERROR, + severityText: 'error', + body: 'some_log_body', + spanContext: { + traceFlags: TraceFlags.SAMPLED, + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '5e107261f64fa53e', + }, +}; +export function ensureExportedAttributesAreCorrect(attributes: IKeyValue[]) { + assert.deepStrictEqual( + attributes, + [ + { + key: 'some-attribute', + value: { + stringValue: 'some attribute value', + }, + }, + ], + 'exported attributes are incorrect' + ); +} + +export function ensureExportedBodyIsCorrect(body?: IAnyValue) { + assert.deepStrictEqual( + body, + { stringValue: 'some_log_body' }, + 'exported attributes are incorrect' + ); +} + +export function ensureExportedLogRecordIsCorrect(logRecord: ILogRecord) { + ensureExportedBodyIsCorrect(logRecord.body); + ensureExportedAttributesAreCorrect(logRecord.attributes); + assert.strictEqual( + logRecord.timeUnixNano, + '1680253513123241728', + 'timeUnixNano is wrong' + ); + assert.strictEqual( + logRecord.observedTimeUnixNano, + '1680253513123241728', + 'observedTimeUnixNano is wrong' + ); + assert.strictEqual( + logRecord.severityNumber, + 'SEVERITY_NUMBER_ERROR', + 'severityNumber is wrong' + ); + assert.strictEqual(logRecord.severityText, 'error', 'severityText is wrong'); + assert.strictEqual( + logRecord.droppedAttributesCount, + 0, + 'droppedAttributesCount is wrong' + ); + assert.strictEqual(logRecord.flags, TraceFlags.SAMPLED, 'flags is wrong'); +} + +export function ensureResourceIsCorrect(resource: IResource) { + assert.deepStrictEqual(resource, { + attributes: [ + { + key: 'service.name', + value: { + stringValue: `unknown_service:${process.argv0}`, + value: 'stringValue', + }, + }, + { + key: 'telemetry.sdk.language', + value: { + stringValue: 'nodejs', + value: 'stringValue', + }, + }, + { + key: 'telemetry.sdk.name', + value: { + stringValue: 'opentelemetry', + value: 'stringValue', + }, + }, + { + key: 'telemetry.sdk.version', + value: { + stringValue: VERSION, + value: 'stringValue', + }, + }, + { + key: 'resource-attribute', + value: { + stringValue: 'some resource-attr value', + value: 'stringValue', + }, + }, + ], + droppedAttributesCount: 0, + }); +} + +export function ensureExportLogsServiceRequestIsSet( + json: IExportLogsServiceRequest +) { + const resourceLogs = json.resourceLogs; + assert.strictEqual(resourceLogs?.length, 1, 'resourceLogs is missing'); + + const resource = resourceLogs?.[0].resource; + assert.ok(resource, 'resource is missing'); + + const scopeLogs = resourceLogs?.[0].scopeLogs; + assert.strictEqual(scopeLogs?.length, 1, 'scopeLogs is missing'); + + const scope = scopeLogs?.[0].scope; + assert.ok(scope, 'scope is missing'); + + const logRecords = scopeLogs?.[0].logRecords; + assert.strictEqual(logRecords?.length, 1, 'logs are missing'); +} + +export class MockedResponse extends Stream { + constructor(private _code: number, private _msg?: string) { + super(); + } + + send(data: string) { + this.emit('data', data); + this.emit('end'); + } + + get statusCode() { + return this._code; + } + + get statusMessage() { + return this._msg; + } +} diff --git a/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts new file mode 100644 index 0000000000..994a2f0ea4 --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/test/node/OTLPLogsExporter.test.ts @@ -0,0 +1,416 @@ +/* + * 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 { ExportResultCode } from '@opentelemetry/core'; +import * as assert from 'assert'; +import * as http from 'http'; +import * as sinon from 'sinon'; +import { Stream, PassThrough } from 'stream'; +import * as zlib from 'zlib'; +import { OTLPLogsExporter } from '../../src'; +import { + ensureExportLogsServiceRequestIsSet, + ensureExportedLogRecordIsCorrect, + mockedReadableLogRecord, + MockedResponse, +} from '../logHelper'; +import { + CompressionAlgorithm, + OTLPExporterNodeConfigBase, + OTLPExporterError, +} from '@opentelemetry/otlp-exporter-base'; +import { + getExportRequestProto, + ServiceClientType, +} from '@opentelemetry/otlp-proto-exporter-base'; +import { IExportLogsServiceRequest } from '@opentelemetry/otlp-transformer'; +import { ReadableLogRecord } from '@opentelemetry/sdk-logs'; + +let fakeRequest: PassThrough; + +describe('OTLPLogsExporter - node with proto over http', () => { + let collectorExporter: OTLPLogsExporter; + let collectorExporterConfig: OTLPExporterNodeConfigBase; + let logs: ReadableLogRecord[]; + + afterEach(() => { + fakeRequest = new Stream.PassThrough(); + 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 OTLPLogsExporter(); + 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 OTLPLogsExporter(); + 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 OTLPLogsExporter(); + 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 OTLPLogsExporter(); + assert.strictEqual( + collectorExporter.url, + envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT + ); + envSource.OTEL_EXPORTER_OTLP_ENDPOINT = ''; + envSource.OTEL_EXPORTER_OTLP_LOGS_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 OTLPLogsExporter(); + 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 OTLPLogsExporter(); + 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 OTLPLogsExporter(); + 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 OTLPLogsExporter(); + assert.strictEqual( + collectorExporter.url, + `${envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT}` + ); + envSource.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT = ''; + }); + it('should use headers defined via env', () => { + envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = 'foo=bar'; + const collectorExporter = new OTLPLogsExporter(); + assert.strictEqual(collectorExporter.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 collectorExporter = new OTLPLogsExporter(); + assert.strictEqual(collectorExporter.headers.foo, 'boo'); + assert.strictEqual(collectorExporter.headers.bar, 'foo'); + envSource.OTEL_EXPORTER_OTLP_LOGS_HEADERS = ''; + envSource.OTEL_EXPORTER_OTLP_HEADERS = ''; + }); + }); + + describe('export', () => { + beforeEach(() => { + collectorExporterConfig = { + headers: { + foo: 'bar', + }, + hostname: 'foo', + url: 'http://foo.bar.com', + keepAlive: true, + httpAgentOptions: { keepAliveMsecs: 2000 }, + }; + collectorExporter = new OTLPLogsExporter(collectorExporterConfig); + logs = []; + logs.push(Object.assign({}, mockedReadableLogRecord)); + }); + afterEach(() => { + sinon.restore(); + }); + + it('should open the connection', done => { + collectorExporter.export(logs, () => {}); + + sinon.stub(http, 'request').callsFake((options: any, cb: any) => { + assert.strictEqual(options.hostname, 'foo.bar.com'); + assert.strictEqual(options.method, 'POST'); + assert.strictEqual(options.path, '/'); + + const mockRes = new MockedResponse(200); + cb(mockRes); + mockRes.send('success'); + done(); + return fakeRequest as any; + }); + }); + + it('should set custom headers', done => { + collectorExporter.export(logs, () => {}); + + sinon.stub(http, 'request').callsFake((options: any, cb: any) => { + assert.strictEqual(options.headers['foo'], 'bar'); + + const mockRes = new MockedResponse(200); + cb(mockRes); + mockRes.send('success'); + done(); + return fakeRequest as any; + }); + }); + + it('should have keep alive and keepAliveMsecs option set', done => { + collectorExporter.export(logs, () => {}); + + sinon.stub(http, 'request').callsFake((options: any, cb: any) => { + assert.strictEqual(options.agent.keepAlive, true); + assert.strictEqual(options.agent.options.keepAliveMsecs, 2000); + + const mockRes = new MockedResponse(200); + cb(mockRes); + mockRes.send('success'); + done(); + return fakeRequest as any; + }); + }); + + it('should successfully send the logs', done => { + const fakeRequest = new Stream.PassThrough(); + sinon.stub(http, 'request').returns(fakeRequest as any); + + let buff = Buffer.from(''); + fakeRequest.on('end', () => { + const ExportLogsServiceRequestProto = getExportRequestProto( + ServiceClientType.LOGS + ); + const data = ExportLogsServiceRequestProto.decode(buff); + const json = data?.toJSON() as IExportLogsServiceRequest; + const log1 = json.resourceLogs?.[0].scopeLogs?.[0].logRecords?.[0]; + assert.ok(typeof log1 !== 'undefined', "log doesn't exist"); + ensureExportedLogRecordIsCorrect(log1); + + ensureExportLogsServiceRequestIsSet(json); + + done(); + }); + + fakeRequest.on('data', chunk => { + buff = Buffer.concat([buff, chunk]); + }); + + const clock = sinon.useFakeTimers(); + collectorExporter.export(logs, () => {}); + clock.tick(200); + clock.restore(); + }); + + it('should log the successful message', done => { + // Need to stub/spy on the underlying logger as the "diag" instance is global + const spyLoggerError = sinon.stub(diag, 'error'); + + collectorExporter.export(logs, result => { + assert.strictEqual(result.code, ExportResultCode.SUCCESS); + assert.strictEqual(spyLoggerError.args.length, 0); + done(); + }); + + sinon.stub(http, 'request').callsFake((options: any, cb: any) => { + const mockRes = new MockedResponse(200); + cb(mockRes); + mockRes.send('success'); + return fakeRequest as any; + }); + }); + + it('should log the error message', done => { + collectorExporter.export(logs, result => { + assert.strictEqual(result.code, ExportResultCode.FAILED); + // @ts-expect-error verify error code + assert.strictEqual(result.error.code, 400); + done(); + }); + + sinon.stub(http, 'request').callsFake((options: any, cb: any) => { + const mockResError = new MockedResponse(400); + cb(mockResError); + mockResError.send('failed'); + + return fakeRequest as any; + }); + }); + }); + describe('export - with compression', () => { + beforeEach(() => { + collectorExporterConfig = { + headers: { + foo: 'bar', + }, + hostname: 'foo', + url: 'http://foo.bar.com', + keepAlive: true, + compression: CompressionAlgorithm.GZIP, + httpAgentOptions: { keepAliveMsecs: 2000 }, + }; + collectorExporter = new OTLPLogsExporter(collectorExporterConfig); + logs = []; + logs.push(Object.assign({}, mockedReadableLogRecord)); + }); + afterEach(() => { + sinon.restore(); + }); + + it('should successfully send the logs', done => { + const fakeRequest = new Stream.PassThrough(); + sinon.stub(http, 'request').returns(fakeRequest as any); + const spySetHeader = sinon.spy(); + (fakeRequest as any).setHeader = spySetHeader; + + let buff = Buffer.from(''); + fakeRequest.on('end', () => { + const unzippedBuff = zlib.gunzipSync(buff); + const ExportLogsServiceRequestProto = getExportRequestProto( + ServiceClientType.LOGS + ); + const data = ExportLogsServiceRequestProto.decode(unzippedBuff); + const json = data?.toJSON() as IExportLogsServiceRequest; + const log1 = json.resourceLogs?.[0].scopeLogs?.[0].logRecords?.[0]; + assert.ok(typeof log1 !== 'undefined', "log doesn't exist"); + ensureExportedLogRecordIsCorrect(log1); + + ensureExportLogsServiceRequestIsSet(json); + assert.ok(spySetHeader.calledWith('Content-Encoding', 'gzip')); + + done(); + }); + + fakeRequest.on('data', chunk => { + buff = Buffer.concat([buff, chunk]); + }); + + const clock = sinon.useFakeTimers(); + collectorExporter.export(logs, () => {}); + clock.tick(200); + clock.restore(); + }); + }); +}); + +describe('export - real http request destroyed before response received', () => { + let collectorExporter: OTLPLogsExporter; + let collectorExporterConfig: OTLPExporterNodeConfigBase; + let logs: ReadableLogRecord[]; + const server = http.createServer((_, res) => { + setTimeout(() => { + res.statusCode = 200; + res.end(); + }, 200); + }); + before(done => { + server.listen(8082, done); + }); + after(done => { + server.close(done); + }); + it('should log the timeout request error message when timeout is 1', done => { + collectorExporterConfig = { + url: 'http://localhost:8082', + timeoutMillis: 1, + }; + collectorExporter = new OTLPLogsExporter(collectorExporterConfig); + logs = []; + logs.push(Object.assign({}, mockedReadableLogRecord)); + + collectorExporter.export(logs, result => { + assert.strictEqual(result.code, ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + done(); + }); + }); + it('should log the timeout request error message when timeout is 100', done => { + collectorExporterConfig = { + url: 'http://localhost:8082', + timeoutMillis: 100, + }; + collectorExporter = new OTLPLogsExporter(collectorExporterConfig); + logs = []; + logs.push(Object.assign({}, mockedReadableLogRecord)); + + collectorExporter.export(logs, result => { + assert.strictEqual(result.code, ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + done(); + }); + }); +}); + +describe('export - real http request destroyed after response received', () => { + let collectorExporter: OTLPLogsExporter; + let collectorExporterConfig: OTLPExporterNodeConfigBase; + let logs: ReadableLogRecord[]; + + const server = http.createServer((_, res) => { + res.write('writing something'); + }); + before(done => { + server.listen(8082, done); + }); + after(done => { + server.close(done); + }); + it('should log the timeout request error message', done => { + collectorExporterConfig = { + url: 'http://localhost:8082', + timeoutMillis: 300, + }; + collectorExporter = new OTLPLogsExporter(collectorExporterConfig); + logs = []; + logs.push(Object.assign({}, mockedReadableLogRecord)); + + collectorExporter.export(logs, result => { + assert.strictEqual(result.code, ExportResultCode.FAILED); + const error = result.error as OTLPExporterError; + assert.ok(error !== undefined); + assert.strictEqual(error.message, 'Request Timeout'); + done(); + }); + }); +}); diff --git a/experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json new file mode 100644 index 0000000000..5eba4694be --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json @@ -0,0 +1,40 @@ +{ + "extends": "../../../tsconfig.base.esm.json", + "compilerOptions": { + "outDir": "build/esm", + "rootDir": "src", + "tsBuildInfoFile": "build/esm/tsconfig.esm.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-resources" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + }, + { + "path": "../api-logs" + }, + { + "path": "../otlp-exporter-base" + }, + { + "path": "../otlp-proto-exporter-base" + }, + { + "path": "../otlp-transformer" + }, + { + "path": "../sdk-logs" + } + ] +} diff --git a/experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json new file mode 100644 index 0000000000..b37520a70f --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json @@ -0,0 +1,40 @@ +{ + "extends": "../../../tsconfig.base.esnext.json", + "compilerOptions": { + "outDir": "build/esnext", + "rootDir": "src", + "tsBuildInfoFile": "build/esnext/tsconfig.esnext.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-resources" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + }, + { + "path": "../api-logs" + }, + { + "path": "../otlp-exporter-base" + }, + { + "path": "../otlp-proto-exporter-base" + }, + { + "path": "../otlp-transformer" + }, + { + "path": "../sdk-logs" + } + ] +} diff --git a/experimental/packages/exporter-logs-otlp-proto/tsconfig.json b/experimental/packages/exporter-logs-otlp-proto/tsconfig.json new file mode 100644 index 0000000000..2349f8706c --- /dev/null +++ b/experimental/packages/exporter-logs-otlp-proto/tsconfig.json @@ -0,0 +1,40 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "." + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../packages/opentelemetry-core" + }, + { + "path": "../../../packages/opentelemetry-resources" + }, + { + "path": "../../../packages/opentelemetry-sdk-trace-base" + }, + { + "path": "../api-logs" + }, + { + "path": "../otlp-exporter-base" + }, + { + "path": "../otlp-proto-exporter-base" + }, + { + "path": "../otlp-transformer" + }, + { + "path": "../sdk-logs" + } + ] +} diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts index 4e9f95d5c0..34c80d6e82 100644 --- a/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/browser/OTLPProtoExporterBrowserBase.ts @@ -22,13 +22,8 @@ import { OTLPExporterConfigBase, sendWithXhr, } from '@opentelemetry/otlp-exporter-base'; -import * as root from '../../generated/root'; -interface ExportRequestType unknown }> { - create(properties?: T): R; - encode(message: T, writer?: protobuf.Writer): protobuf.Writer; - decode(reader: protobuf.Reader | Uint8Array, length?: number): R; -} +import { getExportRequestProto } from '../util'; /** * Collector Exporter abstract base class @@ -41,18 +36,6 @@ export abstract class OTLPProtoExporterBrowserBase< super(config); } - private _getExportRequestProto( - clientType: ServiceClientType - ): ExportRequestType { - if (clientType === ServiceClientType.SPANS) { - // eslint-disable-next-line - return root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest as unknown as ExportRequestType; - } else { - // eslint-disable-next-line - return root.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest as unknown as ExportRequestType; - } - } - override send( objects: ExportItem[], onSuccess: () => void, @@ -64,7 +47,7 @@ export abstract class OTLPProtoExporterBrowserBase< } const serviceRequest = this.convert(objects); - const exportRequestType = this._getExportRequestProto( + const exportRequestType = getExportRequestProto( this.getServiceClientType() ); const message = exportRequestType.create(serviceRequest); diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts index 22efeb0309..fc344756ea 100644 --- a/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/index.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -export { - OTLPProtoExporterNodeBase, - ExportRequestType, - getExportRequestProto, - send, -} from './node'; +export { OTLPProtoExporterNodeBase, send } from './node'; export { OTLPProtoExporterBrowserBase } from './browser'; export { ServiceClientType } from './types'; +export { ExportRequestType, getExportRequestProto } from './util'; diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts index c9c84bdac3..08016fec93 100644 --- a/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/node/index.ts @@ -15,4 +15,4 @@ */ export { OTLPProtoExporterNodeBase } from './OTLPProtoExporterNodeBase'; -export { ExportRequestType, getExportRequestProto, send } from './util'; +export { send } from './util'; diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts index 5cdbfa396d..36fe866d67 100644 --- a/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/node/util.ts @@ -14,33 +14,14 @@ * limitations under the License. */ -import { ServiceClientType } from '../types'; import { OTLPProtoExporterNodeBase } from './OTLPProtoExporterNodeBase'; import { CompressionAlgorithm, OTLPExporterError, sendWithHttp, } from '@opentelemetry/otlp-exporter-base'; -import type * as protobuf from 'protobufjs'; -import * as root from '../../generated/root'; -export interface ExportRequestType unknown }> { - create(properties?: T): R; - encode(message: T, writer?: protobuf.Writer): protobuf.Writer; - decode(reader: protobuf.Reader | Uint8Array, length?: number): R; -} - -export function getExportRequestProto( - clientType: ServiceClientType -): ExportRequestType { - if (clientType === ServiceClientType.SPANS) { - return root.opentelemetry.proto.collector.trace.v1 - .ExportTraceServiceRequest as unknown as ExportRequestType; - } else { - return root.opentelemetry.proto.collector.metrics.v1 - .ExportMetricsServiceRequest as unknown as ExportRequestType; - } -} +import { getExportRequestProto } from '../util'; export function send( collector: OTLPProtoExporterNodeBase, diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts index 389cfb1f52..b15ad36abc 100644 --- a/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/types.ts @@ -17,4 +17,5 @@ export enum ServiceClientType { SPANS, METRICS, + LOGS, } diff --git a/experimental/packages/otlp-proto-exporter-base/src/platform/util.ts b/experimental/packages/otlp-proto-exporter-base/src/platform/util.ts new file mode 100644 index 0000000000..c6e1272816 --- /dev/null +++ b/experimental/packages/otlp-proto-exporter-base/src/platform/util.ts @@ -0,0 +1,39 @@ +/* + * 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 root from '../generated/root'; +import { ServiceClientType } from './types'; + +export interface ExportRequestType unknown }> { + create(properties?: T): R; + encode(message: T, writer?: protobuf.Writer): protobuf.Writer; + decode(reader: protobuf.Reader | Uint8Array, length?: number): R; +} + +export function getExportRequestProto( + clientType: ServiceClientType +): ExportRequestType { + if (clientType === ServiceClientType.SPANS) { + return root.opentelemetry.proto.collector.trace.v1 + .ExportTraceServiceRequest as unknown as ExportRequestType; + } else if (clientType === ServiceClientType.LOGS) { + return root.opentelemetry.proto.collector.logs.v1 + .ExportLogsServiceRequest as unknown as ExportRequestType; + } else { + return root.opentelemetry.proto.collector.metrics.v1 + .ExportMetricsServiceRequest as unknown as ExportRequestType; + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json index e10c926214..2fec210b9a 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -14,6 +14,9 @@ { "path": "experimental/packages/exporter-logs-otlp-http/tsconfig.esm.json" }, + { + "path": "experimental/packages/exporter-logs-otlp-proto/tsconfig.esm.json" + }, { "path": "experimental/packages/exporter-trace-otlp-http/tsconfig.esm.json" }, diff --git a/tsconfig.esnext.json b/tsconfig.esnext.json index 507c4d4309..c5970c9c5c 100644 --- a/tsconfig.esnext.json +++ b/tsconfig.esnext.json @@ -14,6 +14,9 @@ { "path": "experimental/packages/exporter-logs-otlp-http/tsconfig.esnext.json" }, + { + "path": "experimental/packages/exporter-logs-otlp-proto/tsconfig.esnext.json" + }, { "path": "experimental/packages/exporter-trace-otlp-http/tsconfig.esnext.json" }, diff --git a/tsconfig.json b/tsconfig.json index 7e90e75d2d..acf7329087 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "experimental/packages/api-logs", "experimental/packages/exporter-logs-otlp-grpc", "experimental/packages/exporter-logs-otlp-http", + "experimental/packages/exporter-logs-otlp-proto", "experimental/packages/exporter-trace-otlp-grpc", "experimental/packages/exporter-trace-otlp-http", "experimental/packages/exporter-trace-otlp-proto", @@ -71,6 +72,9 @@ { "path": "experimental/packages/exporter-logs-otlp-http" }, + { + "path": "experimental/packages/exporter-logs-otlp-proto" + }, { "path": "experimental/packages/exporter-trace-otlp-grpc" },