From 0a00912a1fc53ec97ab5a6f3a29b23c6dc157608 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 17 Jan 2023 18:31:48 +0800 Subject: [PATCH 01/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/.eslintignore | 1 + experimental/packages/sdk-logs/.eslintrc.js | 7 + experimental/packages/sdk-logs/.npmignore | 4 + experimental/packages/sdk-logs/LICENSE | 201 ++++++++++++++++++ experimental/packages/sdk-logs/README.md | 91 ++++++++ experimental/packages/sdk-logs/karma.conf.js | 26 +++ experimental/packages/sdk-logs/package.json | 97 +++++++++ .../packages/sdk-logs/src/LogRecord.ts | 167 +++++++++++++++ .../sdk-logs/src/LogRecordProcessor.ts | 36 ++++ experimental/packages/sdk-logs/src/Logger.ts | 28 +++ .../packages/sdk-logs/src/LoggerProvider.ts | 104 +++++++++ .../sdk-logs/src/LoggerSharedState.ts | 28 +++ .../sdk-logs/src/MultiLogRecordProcessor.ts | 47 ++++ experimental/packages/sdk-logs/src/config.ts | 29 +++ .../src/export/BatchLogRecordProcessor.ts | 147 +++++++++++++ .../src/export/ConsoleLogRecordExporter.ts | 75 +++++++ .../src/export/InMemoryLogRecordExporter.ts | 48 +++++ .../sdk-logs/src/export/LogRecordExporter.ts | 30 +++ .../sdk-logs/src/export/ReadableLogRecord.ts | 34 +++ .../src/export/SimpleLogRecordProcessor.ts | 46 ++++ experimental/packages/sdk-logs/src/index.ts | 30 +++ .../browser/export/BatchLogRecordProcessor.ts | 59 +++++ .../sdk-logs/src/platform/browser/index.ts | 17 ++ .../packages/sdk-logs/src/platform/index.ts | 17 ++ .../node/export/BatchLogRecordProcessor.ts | 22 ++ .../sdk-logs/src/platform/node/index.ts | 17 ++ .../sdk-logs/src/semantic-conventions.ts | 20 ++ experimental/packages/sdk-logs/src/types.ts | 73 +++++++ .../packages/sdk-logs/tsconfig.esm.json | 25 +++ .../packages/sdk-logs/tsconfig.esnext.json | 25 +++ experimental/packages/sdk-logs/tsconfig.json | 25 +++ lerna.json | 1 + packages/opentelemetry-core/src/index.ts | 1 + .../opentelemetry-core/src/utils/timeout.ts | 64 ++++++ tsconfig.esm.json | 3 + tsconfig.esnext.json | 3 + tsconfig.json | 4 + 37 files changed, 1652 insertions(+) create mode 100644 experimental/packages/sdk-logs/.eslintignore create mode 100644 experimental/packages/sdk-logs/.eslintrc.js create mode 100644 experimental/packages/sdk-logs/.npmignore create mode 100644 experimental/packages/sdk-logs/LICENSE create mode 100644 experimental/packages/sdk-logs/README.md create mode 100644 experimental/packages/sdk-logs/karma.conf.js create mode 100644 experimental/packages/sdk-logs/package.json create mode 100644 experimental/packages/sdk-logs/src/LogRecord.ts create mode 100644 experimental/packages/sdk-logs/src/LogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/src/Logger.ts create mode 100644 experimental/packages/sdk-logs/src/LoggerProvider.ts create mode 100644 experimental/packages/sdk-logs/src/LoggerSharedState.ts create mode 100644 experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/src/config.ts create mode 100644 experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts create mode 100644 experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts create mode 100644 experimental/packages/sdk-logs/src/export/LogRecordExporter.ts create mode 100644 experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts create mode 100644 experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/src/index.ts create mode 100644 experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/src/platform/browser/index.ts create mode 100644 experimental/packages/sdk-logs/src/platform/index.ts create mode 100644 experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/src/platform/node/index.ts create mode 100644 experimental/packages/sdk-logs/src/semantic-conventions.ts create mode 100644 experimental/packages/sdk-logs/src/types.ts create mode 100644 experimental/packages/sdk-logs/tsconfig.esm.json create mode 100644 experimental/packages/sdk-logs/tsconfig.esnext.json create mode 100644 experimental/packages/sdk-logs/tsconfig.json create mode 100644 packages/opentelemetry-core/src/utils/timeout.ts diff --git a/experimental/packages/sdk-logs/.eslintignore b/experimental/packages/sdk-logs/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/experimental/packages/sdk-logs/.eslintignore @@ -0,0 +1 @@ +build diff --git a/experimental/packages/sdk-logs/.eslintrc.js b/experimental/packages/sdk-logs/.eslintrc.js new file mode 100644 index 0000000000..029ade4634 --- /dev/null +++ b/experimental/packages/sdk-logs/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + env: { + mocha: true, + node: true, + }, + ...require("../../../eslint.config.js"), +}; diff --git a/experimental/packages/sdk-logs/.npmignore b/experimental/packages/sdk-logs/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/experimental/packages/sdk-logs/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/experimental/packages/sdk-logs/LICENSE b/experimental/packages/sdk-logs/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/experimental/packages/sdk-logs/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/sdk-logs/README.md b/experimental/packages/sdk-logs/README.md new file mode 100644 index 0000000000..76bb6d4eac --- /dev/null +++ b/experimental/packages/sdk-logs/README.md @@ -0,0 +1,91 @@ +# OpenTelemetry Logs SDK + +[![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.** + +OpenTelemetry logs module contains the foundation for all logs SDKs of [opentelemetry-js](https://github.com/open-telemetry/opentelemetry-js). + +Used standalone, this module provides methods for manual instrumentation of code, offering full control over recording logs for client-side JavaScript (browser) and Node.js. + +It does **not** provide automated instrumentation of known libraries or host environment logs out-of-the-box. + +## Installation + +```bash +npm install --save @opentelemetry/api-logs +npm install --save @opentelemetry/sdk-logs +``` + +## Usage + +The basic setup of the SDK can be seen as followings: + +```js +const logsAPI = require("@opentelemetry/api-logs"); +const { + LoggerProvider, + SimpleLogRecordProcessor, + ConsoleLogRecordExporter, +} = require("@opentelemetry/sdk-logs"); + +// To start a logger, you first need to initialize the Logger provider. +const loggerProvider = new LoggerProvider(); +// Add a processor to export log record +loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()) +); + +// To create a log record, you first need to get a Logger instance +const logger = loggerProvider.getLogger("default"); + +// You can also use global singleton +logsAPI.logs.setGlobalLoggerProvider(loggerProvider); +const logger = logsAPI.logs.getLogger("default"); + +// logging an event in an instrumentation library +logger + .getLogEvent("event-name", { + severityNumber: SeverityNumber.WARN, + severityText: "WARN", + body: "this is a log event body", + attributes: { "log.type": "LogEvent" }, + }) + .emit(); + +// logging an event in a log appender +logger + .getLogRecord({ + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + body: "this is a log record body", + attributes: { "log.type": "LogRecord" }, + }) + .emit(); +``` + +## Config + +Logs configuration is a merge of user supplied configuration with both the default +configuration as specified in [config.ts](./src/config.ts) + +## Example + +See [examples/logs](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/examples/logs) + +## 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/sdk-logs +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fmetrics.svg diff --git a/experimental/packages/sdk-logs/karma.conf.js b/experimental/packages/sdk-logs/karma.conf.js new file mode 100644 index 0000000000..4a4a8ff59f --- /dev/null +++ b/experimental/packages/sdk-logs/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, + }) + ); +}; diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json new file mode 100644 index 0000000000..6467e7ba6e --- /dev/null +++ b/experimental/packages/sdk-logs/package.json @@ -0,0 +1,97 @@ +{ + "name": "@opentelemetry/sdk-logs", + "version": "0.34.0", + "publishConfig": { + "access": "public" + }, + "description": "OpenTelemetry logs SDK", + "author": "OpenTelemetry Authors", + "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/sdk-logs", + "license": "Apache-2.0", + "main": "build/src/index.js", + "module": "build/esm/index.js", + "esnext": "build/esnext/index.js", + "types": "build/src/index.d.ts", + "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" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/open-telemetry/opentelemetry-js.git" + }, + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js/issues" + }, + "engines": { + "node": ">=14" + }, + "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", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "test:browser": "nyc karma start --single-run", + "tdd": "npm run test -- --watch-extensions ts --watch", + "tdd:browser": "karma start", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "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": "node ../../../scripts/version-update.js", + "peer-api-check": "node ../../../scripts/peer-api-check.js" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "logs", + "stats", + "profiling" + ], + "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", + "doc", + "LICENSE", + "README.md" + ], + "sideEffects": false, + "devDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.4.0", + "@types/mocha": "10.0.0", + "@types/node": "18.6.5", + "@types/sinon": "10.0.13", + "codecov": "3.8.3", + "karma": "6.3.16", + "karma-chrome-launcher": "3.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-mocha": "2.0.1", + "karma-spec-reporter": "0.0.32", + "karma-webpack": "4.0.2", + "mocha": "10.0.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "sinon": "14.0.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.4.0" + }, + "dependencies": { + "@opentelemetry/core": "1.8.0", + "@opentelemetry/resources": "1.8.0", + "@opentelemetry/api-logs": "0.34.0" + } +} diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts new file mode 100644 index 0000000000..03f0919bac --- /dev/null +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -0,0 +1,167 @@ +/* + * 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 type { Attributes, AttributeValue } from '@opentelemetry/api'; +import type * as logsAPI from '@opentelemetry/api-logs'; +import type { InstrumentationScope } from '@opentelemetry/core'; +import * as api from '@opentelemetry/api'; +import { + hrTime, + timeInputToHrTime, + isAttributeValue, +} from '@opentelemetry/core'; + +import { Resource } from '@opentelemetry/resources'; +import { ReadableLogRecord } from './export/ReadableLogRecord'; +import { LoggerConfig } from './types'; + +export class LogRecord implements ReadableLogRecord { + readonly time: api.HrTime; + readonly observedTime?: api.HrTime; + readonly traceId?: string; + readonly spanId?: string; + readonly traceFlags?: number; + readonly severityText?: string; + readonly severityNumber?: logsAPI.SeverityNumber; + readonly body?: string; + readonly resource: Resource; + readonly instrumentationScope: InstrumentationScope; + readonly attributes: Attributes = {}; + + private _isEmitted = false; + + constructor( + private readonly config: LoggerConfig, + logRecord: logsAPI.LogRecord + ) { + const { + time = hrTime(), + observedTime, + // if includeTraceContext is true, Gets the current trace context by default + context = this.config.includeTraceContext + ? api.context.active() + : undefined, + severityNumber, + severityText, + body, + attributes = {}, + } = logRecord; + this.time = timeInputToHrTime(time); + this.observedTime = + observedTime === undefined + ? observedTime + : timeInputToHrTime(observedTime); + + if (context) { + const spanContext = api.trace.getSpanContext(context); + if (spanContext && api.isSpanContextValid(spanContext)) { + this.spanId = spanContext.spanId; + this.traceId = spanContext.traceId; + this.traceFlags = spanContext.traceFlags; + } + } + + this.severityNumber = severityNumber; + this.severityText = severityText; + this.body = body; + this.resource = this.config.loggerSharedState.resource; + this.instrumentationScope = this.config.instrumentationScope; + this.setAttributes(attributes); + } + + public emit(): void { + if (this.config.loggerSharedState.shutdownOnceFeature.isCalled) { + api.diag.warn('can not emit, it is already shutdown'); + return; + } + if (this._isLogRecordEmitted()) { + return; + } + this._isEmitted = true; + this.config.loggerSharedState.activeProcessor.onEmit(this); + } + + private setAttribute(key: string, value?: AttributeValue): LogRecord { + if (value === null || this._isLogRecordEmitted()) { + return this; + } + if (key.length === 0) { + api.diag.warn(`Invalid attribute key: ${key}`); + return this; + } + if (!isAttributeValue(value)) { + api.diag.warn(`Invalid attribute value set for key: ${key}`); + return this; + } + if ( + Object.keys(this.attributes).length >= + this.config.loggerSharedState.logRecordLimits.attributeCountLimit! && + !Object.prototype.hasOwnProperty.call(this.attributes, key) + ) { + return this; + } + this.attributes[key] = this._truncateToSize(value); + return this; + } + + private setAttributes(attributes: Attributes): LogRecord { + for (const [k, v] of Object.entries(attributes)) { + this.setAttribute(k, v); + } + return this; + } + + private _isLogRecordEmitted(): boolean { + if (this._isEmitted) { + api.diag.warn('Can not execute the operation on emitted LogRecord'); + } + return this._isEmitted; + } + + private _truncateToSize(value: AttributeValue): AttributeValue { + const limit = + this.config.loggerSharedState.logRecordLimits.attributeValueLengthLimit || + 0; + // Check limit + if (limit <= 0) { + // Negative values are invalid, so do not truncate + api.diag.warn(`Attribute value limit must be positive, got ${limit}`); + return value; + } + + // String + if (typeof value === 'string') { + return this._truncateToLimitUtil(value, limit); + } + + // Array of strings + if (Array.isArray(value)) { + return (value as []).map(val => + typeof val === 'string' ? this._truncateToLimitUtil(val, limit) : val + ); + } + + // Other types, no need to apply value length limit + return value; + } + + private _truncateToLimitUtil(value: string, limit: number): string { + if (value.length <= limit) { + return value; + } + return value.substr(0, limit); + } +} diff --git a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts new file mode 100644 index 0000000000..8dae4eb691 --- /dev/null +++ b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts @@ -0,0 +1,36 @@ +/* + * 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 type { ReadableLogRecord } from "./export/ReadableLogRecord"; + +export interface LogRecordProcessor { + /** + * Forces to export all finished log records + */ + forceFlush(): Promise; + + /** + * Called when a {@link ReadableLogRecord} is emit + * @param logRecord the ReadableLogRecord that just emitted. + */ + onEmit(logRecord: ReadableLogRecord): void; + + /** + * Shuts down the processor. Called when SDK is shut down. This is an + * opportunity for processor to do any cleanup required. + */ + shutdown(): Promise; +} diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts new file mode 100644 index 0000000000..e8fbe75a55 --- /dev/null +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -0,0 +1,28 @@ +/* + * 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 type * as logsAPI from '@opentelemetry/api-logs'; + +import type { LoggerConfig } from './types'; +import { LogRecord } from './LogRecord'; + +export class Logger implements logsAPI.Logger { + constructor(private readonly config: LoggerConfig) {} + + public emitLogRecord(logRecord: logsAPI.LogRecord): void { + new LogRecord(this.config, logRecord).emit(); + } +} diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts new file mode 100644 index 0000000000..1fb7d47a33 --- /dev/null +++ b/experimental/packages/sdk-logs/src/LoggerProvider.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 type * as logsAPI from '@opentelemetry/api-logs'; +import { Resource } from '@opentelemetry/resources'; +import { BindOnceFuture, merge } from '@opentelemetry/core'; + +import type { LoggerProviderConfig } from './types'; +import type { LogRecordProcessor } from './LogRecordProcessor'; +import type { LoggerSharedState } from './LoggerSharedState'; +import { Logger } from './Logger'; +import { loadDefaultConfig } from './config'; +import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; + +export class LoggerProvider implements logsAPI.LoggerProvider { + private readonly _loggerSharedState: LoggerSharedState; + private readonly _loggers: Map = new Map(); + private _shutdownOnceFeature: BindOnceFuture; + + constructor(config: LoggerProviderConfig = {}) { + const { resource, logRecordLimits, forceFlushTimeoutMillis } = merge( + {}, + loadDefaultConfig(), + config + ); + this._shutdownOnceFeature = new BindOnceFuture(this._showdownFeature, this); + this._loggerSharedState = { + resource: Resource.default().merge(resource ?? Resource.empty()), + activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), + shutdownOnceFeature: this._shutdownOnceFeature, + logRecordLimits, + }; + } + + /** + * Get a logger with the configuration of the LoggerProvider. + */ + public getLogger( + name: string, + version?: string, + options?: logsAPI.LoggerOptions + ): Logger { + const { schemaUrl = '', includeTraceContext = true } = options || {}; + const key = `${name}@${version || ''}:${schemaUrl}`; + if (!this._loggers.has(key)) { + this._loggers.set( + key, + new Logger({ + loggerSharedState: this._loggerSharedState, + instrumentationScope: { name, version, schemaUrl }, + includeTraceContext, + }) + ); + } + return this._loggers.get(key)!; + } + + /** + * Adds a new {@link LogRecordProcessor} to this logger. + * @param processor the new LogRecordProcessor to be added. + */ + public addLogRecordProcessor(processor: LogRecordProcessor) { + this._loggerSharedState.activeProcessor.addLogRecordProcessor(processor); + } + + /** + * Notifies all registered LogRecordProcessor to flush any buffered data. + * + * Returns a promise which is resolved when all flushes are complete. + */ + public forceFlush(): Promise { + if (this._shutdownOnceFeature.isCalled) { + return Promise.reject('can not flush, it is already shutdown'); + } + return this._loggerSharedState.activeProcessor.forceFlush(); + } + + /** + * Flush all buffered data and shut down the LoggerProvider and all registered + * LogRecordProcessor. + * + * Returns a promise which is resolved when all flushes are complete. + */ + public shutdown(): Promise { + return this._shutdownOnceFeature.call(); + } + + private _showdownFeature(): Promise { + return this._loggerSharedState.activeProcessor.shutdown(); + } +} diff --git a/experimental/packages/sdk-logs/src/LoggerSharedState.ts b/experimental/packages/sdk-logs/src/LoggerSharedState.ts new file mode 100644 index 0000000000..e36ddd05c4 --- /dev/null +++ b/experimental/packages/sdk-logs/src/LoggerSharedState.ts @@ -0,0 +1,28 @@ +/* + * 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 type { BindOnceFuture } from '@opentelemetry/core'; +import type { Resource } from '@opentelemetry/resources'; + +import type { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; +import type { LogRecordLimits } from './types'; + +export interface LoggerSharedState { + readonly activeProcessor: MultiLogRecordProcessor; + readonly resource: Resource; + readonly logRecordLimits: LogRecordLimits; + readonly shutdownOnceFeature: BindOnceFuture; +} diff --git a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts new file mode 100644 index 0000000000..cde9ba49de --- /dev/null +++ b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts @@ -0,0 +1,47 @@ +/* + * 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 { callWithTimeout } from "@opentelemetry/core"; + +import type { LogRecordProcessor } from "./LogRecordProcessor"; +import type { ReadableLogRecord } from "./export/ReadableLogRecord"; + +/** + * Implementation of the {@link LogRecordProcessor} that simply forwards all + * received events to a list of {@link LogRecordProcessor}s. + */ +export class MultiLogRecordProcessor implements LogRecordProcessor { + private readonly _processors: LogRecordProcessor[] = []; + + constructor(private readonly _forceFlushTimeoutMillis: number) {} + + public addLogRecordProcessor(processor: LogRecordProcessor) { + this._processors.push(processor); + } + + public async forceFlush(): Promise { + const timeout = this._forceFlushTimeoutMillis; + await Promise.all(this._processors.map((processor) => callWithTimeout(processor.forceFlush(), timeout))); + } + + public onEmit(logRecord: ReadableLogRecord): void { + this._processors.forEach((processors) => processors.onEmit(logRecord)); + } + + public async shutdown(): Promise { + await Promise.all(this._processors.map((processor) => processor.shutdown())); + } +} diff --git a/experimental/packages/sdk-logs/src/config.ts b/experimental/packages/sdk-logs/src/config.ts new file mode 100644 index 0000000000..0e8e11c1db --- /dev/null +++ b/experimental/packages/sdk-logs/src/config.ts @@ -0,0 +1,29 @@ +/* + * 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"; + +export function loadDefaultConfig() { + return { + forceFlushTimeoutMillis: 30000, + logRecordLimits: { + attributeValueLengthLimit: getEnv().OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, + attributeCountLimit: getEnv().OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT, + }, + }; +} + +export const DEFAULT_EVENT_DOMAIN = "default"; diff --git a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts new file mode 100644 index 0000000000..aa4e96dc06 --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts @@ -0,0 +1,147 @@ +/* + * 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 type { ExportResult } from "@opentelemetry/core"; +import { diag } from "@opentelemetry/api"; +import { ExportResultCode, getEnv, globalErrorHandler, unrefTimer, callWithTimeout } from "@opentelemetry/core"; + +import type { BufferConfig } from "../types"; +import type { ReadableLogRecord } from "./ReadableLogRecord"; +import type { LogRecordExporter } from "./LogRecordExporter"; +import type { LogRecordProcessor } from "./../LogRecordProcessor"; + +export abstract class BatchLogRecordProcessorBase implements LogRecordProcessor { + private readonly _maxExportBatchSize: number; + private readonly _maxQueueSize: number; + private readonly _scheduledDelayMillis: number; + private readonly _exportTimeoutMillis: number; + + private _finishedLogRecords: ReadableLogRecord[] = []; + private _timer: NodeJS.Timeout | undefined; + + constructor(private readonly _exporter: LogRecordExporter, config?: T) { + const env = getEnv(); + this._maxExportBatchSize = config?.maxExportBatchSize ?? env.OTEL_BSP_MAX_EXPORT_BATCH_SIZE; + this._maxQueueSize = config?.maxQueueSize ?? env.OTEL_BSP_MAX_QUEUE_SIZE; + this._scheduledDelayMillis = config?.scheduledDelayMillis ?? env.OTEL_BSP_SCHEDULE_DELAY; + this._exportTimeoutMillis = config?.exportTimeoutMillis ?? env.OTEL_BSP_EXPORT_TIMEOUT; + + if (this._maxExportBatchSize > this._maxQueueSize) { + diag.warn( + "BatchLogRecordProcessor: maxExportBatchSize must be smaller or equal to maxQueueSize, setting maxExportBatchSize to match maxQueueSize" + ); + this._maxExportBatchSize = this._maxQueueSize; + } + } + + public onEmit(logRecord: ReadableLogRecord): void { + this._addToBuffer(logRecord); + } + + public forceFlush(): Promise { + return this._flushAll(); + } + + public async shutdown(): Promise { + this.onShutdown(); + await this._flushAll(); + await this._exporter.shutdown(); + } + + /** Add a LogRecord in the buffer. */ + private _addToBuffer(logRecord: ReadableLogRecord) { + if (this._finishedLogRecords.length >= this._maxQueueSize) { + return; + } + this._finishedLogRecords.push(logRecord); + this._maybeStartTimer(); + } + + /** + * Send all LogRecords to the exporter respecting the batch size limit + * This function is used only on forceFlush or shutdown, + * for all other cases _flush should be used + * */ + private _flushAll(): Promise { + return new Promise((resolve, reject) => { + const promises = []; + const batchCount = Math.ceil(this._finishedLogRecords.length / this._maxExportBatchSize); + for (let i = 0; i < batchCount; i++) { + promises.push(this._flushOneBatch()); + } + Promise.all(promises) + .then(() => { + resolve(); + }) + .catch(reject); + }); + } + + private _flushOneBatch(): Promise { + this._clearTimer(); + if (this._finishedLogRecords.length === 0) { + return Promise.resolve(); + } + return new Promise((resolve, reject) => { + callWithTimeout( + this._export(this._finishedLogRecords.splice(0, this._maxExportBatchSize)), + this._exportTimeoutMillis + ) + .then(() => resolve()) + .catch(reject); + }); + } + + private _maybeStartTimer() { + if (this._timer !== undefined) { + return; + } + this._timer = setTimeout(() => { + this._flushOneBatch() + .then(() => { + if (this._finishedLogRecords.length > 0) { + this._clearTimer(); + this._maybeStartTimer(); + } + }) + .catch((e) => { + globalErrorHandler(e); + }); + }, this._scheduledDelayMillis); + unrefTimer(this._timer); + } + + private _clearTimer() { + if (this._timer !== undefined) { + clearTimeout(this._timer); + this._timer = undefined; + } + } + + private _export(logRecords: ReadableLogRecord[]): Promise { + return new Promise((resolve, reject) => { + this._exporter.export(logRecords, (res: ExportResult) => { + if (res.code !== ExportResultCode.SUCCESS) { + reject(res.error ?? new Error(`BatchLogRecordProcessorBase: log record export failed (status ${res})`)); + return; + } + resolve(res); + }); + }); + } + + protected abstract onShutdown(): void; +} diff --git a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts new file mode 100644 index 0000000000..6a21e0850d --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts @@ -0,0 +1,75 @@ +/* + * 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 { ExportResult, hrTimeToMicroseconds } from '@opentelemetry/core'; +import { ExportResultCode } from '@opentelemetry/core'; + +import type { ReadableLogRecord } from './ReadableLogRecord'; +import type { LogRecordExporter } from './LogRecordExporter'; + +/** + * This is implementation of {@link LogRecordExporter} that prints LogRecords to the + * console. This class can be used for diagnostic purposes. + */ +export class ConsoleLogRecordExporter implements LogRecordExporter { + /** + * Export logs. + * @param logs + * @param resultCallback + */ + public export( + logs: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void + ) { + this._sendLogRecords(logs).then(res => resultCallback(res)); + } + + /** + * Shutdown the exporter. + */ + public shutdown(): Promise { + return Promise.resolve(); + } + + private _sendLogRecords( + logRecords: ReadableLogRecord[] + ): Promise { + for (const logRecord of logRecords) { + console.dir(this._exportInfo(logRecord), { depth: 3 }); + } + return Promise.resolve({ code: ExportResultCode.SUCCESS }); + } + + /** + * converts logRecord info into more readable format + * @param span + */ + private _exportInfo(logRecord: ReadableLogRecord) { + return { + timestamp: hrTimeToMicroseconds(logRecord.time), + observedTimestamp: logRecord.observedTime + ? hrTimeToMicroseconds(logRecord.observedTime) + : undefined, + traceId: logRecord.traceId, + spanId: logRecord.spanId, + traceFlags: logRecord.traceFlags, + severityText: logRecord.severityText, + severityNumber: logRecord.severityNumber, + body: logRecord.body, + attributes: logRecord.attributes, + }; + } +} diff --git a/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts new file mode 100644 index 0000000000..03b0cc6da4 --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts @@ -0,0 +1,48 @@ +/* + * 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 type { ExportResult } from "@opentelemetry/core"; +import { ExportResultCode } from "@opentelemetry/core"; + +import type { ReadableLogRecord } from "./ReadableLogRecord"; +import type { LogRecordExporter } from "./LogRecordExporter"; + +/** + * This class can be used for testing purposes. It stores the exported LogRecords + * in a list in memory that can be retrieved using the `getFinishedLogRecords()` + * method. + */ +export class InMemoryLogRecordExporter implements LogRecordExporter { + private _finishedLogRecords: ReadableLogRecord[] = []; + + public export(logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void) { + this._finishedLogRecords.push(...logs); + resultCallback({ code: ExportResultCode.SUCCESS }); + } + + public shutdown(): Promise { + this.reset(); + return Promise.resolve(); + } + + public getFinishedLogRecords(): ReadableLogRecord[] { + return this._finishedLogRecords; + } + + public reset(): void { + this._finishedLogRecords = []; + } +} diff --git a/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts new file mode 100644 index 0000000000..0738f46c44 --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts @@ -0,0 +1,30 @@ +/* + * 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 type { ExportResult } from "@opentelemetry/core"; + +import type { ReadableLogRecord } from "./ReadableLogRecord"; + +export interface LogRecordExporter { + /** + * Called to export {@link ReadableLogRecord}s. + * @param logs the list of sampled LogRecords to be exported. + */ + export(logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void): void; + + /** Stops the exporter. */ + shutdown(): Promise; +} diff --git a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts new file mode 100644 index 0000000000..25163878fe --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts @@ -0,0 +1,34 @@ +/* + * 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 type { Resource } from '@opentelemetry/resources'; +import type { Attributes, HrTime } from '@opentelemetry/api'; +import type { InstrumentationScope } from '@opentelemetry/core'; +import type { SeverityNumber } from '@opentelemetry/api-logs'; + +export interface ReadableLogRecord { + readonly time: HrTime; + readonly observedTime?: HrTime; + readonly traceId?: string; + readonly spanId?: string; + readonly traceFlags?: number; + readonly severityText?: string; + readonly severityNumber?: SeverityNumber; + readonly body?: string; + readonly resource: Resource; + readonly instrumentationScope: InstrumentationScope; + readonly attributes: Attributes; +} diff --git a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts new file mode 100644 index 0000000000..77d8f3b125 --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.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 type { ExportResult } from "@opentelemetry/core"; +import { ExportResultCode, globalErrorHandler } from "@opentelemetry/core"; + +import type { LogRecordExporter } from "./LogRecordExporter"; +import type { LogRecordProcessor } from "../LogRecordProcessor"; +import type { ReadableLogRecord } from "./ReadableLogRecord"; + +export class SimpleLogRecordProcessor implements LogRecordProcessor { + constructor(private readonly _exporter: LogRecordExporter) {} + + public onEmit(logRecord: ReadableLogRecord): void { + this._exporter.export([logRecord], (res: ExportResult) => { + if (res.code !== ExportResultCode.SUCCESS) { + globalErrorHandler( + res.error ?? new Error(`SimpleLogRecordProcessor: log record export failed (status ${res})`) + ); + return; + } + }); + } + + public forceFlush(): Promise { + // do nothing as all spans are being exported without waiting + return Promise.resolve(); + } + + public shutdown(): Promise { + return this._exporter.shutdown(); + } +} diff --git a/experimental/packages/sdk-logs/src/index.ts b/experimental/packages/sdk-logs/src/index.ts new file mode 100644 index 0000000000..35c31c3433 --- /dev/null +++ b/experimental/packages/sdk-logs/src/index.ts @@ -0,0 +1,30 @@ +/* + * 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 * from "./types"; +export * from "./LoggerProvider"; +export * from "./EventLoggerProvider"; +export * from "./Logger"; +export * from "./EventLogger"; +export * from "./LogRecord"; +export * from "./LogRecordProcessor"; +export * from "./export/ReadableLogRecord"; +export * from "./export/BatchLogRecordProcessor"; +export * from "./export/ConsoleLogRecordExporter"; +export * from "./export/LogRecordExporter"; +export * from "./export/SimpleLogRecordProcessor"; +export * from "./export/InMemoryLogRecordExporter"; +export * from "./platform"; diff --git a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts new file mode 100644 index 0000000000..ee8d6fc00e --- /dev/null +++ b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts @@ -0,0 +1,59 @@ +/* + * 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 type { LogRecordExporter } from "./../../../export/LogRecordExporter"; +import type { BatchLogRecordProcessorBrowserConfig } from "../../../types"; +import { BatchLogRecordProcessorBase } from "../../../export/BatchLogRecordProcessor"; + +export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { + private _visibilityChangeListener?: () => void; + private _pageHideListener?: () => void; + + constructor(exporter: LogRecordExporter, config?: BatchLogRecordProcessorBrowserConfig) { + super(exporter, config); + this._onInit(config); + } + + protected onShutdown(): void { + if (typeof document === "undefined") { + return; + } + if (this._visibilityChangeListener) { + document.removeEventListener("visibilitychange", this._visibilityChangeListener); + } + if (this._pageHideListener) { + document.removeEventListener("pagehide", this._pageHideListener); + } + } + + private _onInit(config?: BatchLogRecordProcessorBrowserConfig): void { + if (config?.disableAutoFlushOnDocumentHide === true || typeof document === "undefined") { + return; + } + this._visibilityChangeListener = () => { + if (document.visibilityState === "hidden") { + this.forceFlush(); + } + }; + this._pageHideListener = () => { + this.forceFlush(); + }; + document.addEventListener("visibilitychange", this._visibilityChangeListener); + + // use 'pagehide' event as a fallback for Safari; see https://bugs.webkit.org/show_bug.cgi?id=116769 + document.addEventListener("pagehide", this._pageHideListener); + } +} diff --git a/experimental/packages/sdk-logs/src/platform/browser/index.ts b/experimental/packages/sdk-logs/src/platform/browser/index.ts new file mode 100644 index 0000000000..5955ac152a --- /dev/null +++ b/experimental/packages/sdk-logs/src/platform/browser/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 * from "./export/BatchLogRecordProcessor"; diff --git a/experimental/packages/sdk-logs/src/platform/index.ts b/experimental/packages/sdk-logs/src/platform/index.ts new file mode 100644 index 0000000000..3949243f75 --- /dev/null +++ b/experimental/packages/sdk-logs/src/platform/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 * from "./node"; diff --git a/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts new file mode 100644 index 0000000000..0319087a30 --- /dev/null +++ b/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts @@ -0,0 +1,22 @@ +/* + * 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 type { BufferConfig } from "../../../types"; +import { BatchLogRecordProcessorBase } from "../../../export/BatchLogRecordProcessor"; + +export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { + protected onShutdown(): void {} +} diff --git a/experimental/packages/sdk-logs/src/platform/node/index.ts b/experimental/packages/sdk-logs/src/platform/node/index.ts new file mode 100644 index 0000000000..5955ac152a --- /dev/null +++ b/experimental/packages/sdk-logs/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 * from "./export/BatchLogRecordProcessor"; diff --git a/experimental/packages/sdk-logs/src/semantic-conventions.ts b/experimental/packages/sdk-logs/src/semantic-conventions.ts new file mode 100644 index 0000000000..56cf1ee7cc --- /dev/null +++ b/experimental/packages/sdk-logs/src/semantic-conventions.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. + */ + +export const EVENT_LOGS_ATTRIBUTES = { + name: "event.name", + domain: "event.domain", +}; diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts new file mode 100644 index 0000000000..7ca29bdb47 --- /dev/null +++ b/experimental/packages/sdk-logs/src/types.ts @@ -0,0 +1,73 @@ +/* + * 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 type { Resource } from '@opentelemetry/resources'; +import type { InstrumentationScope } from '@opentelemetry/core'; + +import type { LoggerSharedState } from './LoggerSharedState'; + +export interface LoggerProviderConfig { + /** Resource associated with trace telemetry */ + resource?: Resource; + + /** Log Record Limits*/ + logRecordLimits?: LogRecordLimits; + + /** + * How long the forceFlush can run before it is cancelled. + * The default value is 30000ms + */ + forceFlushTimeoutMillis?: number; +} + +export interface LogRecordLimits { + /** attributeValueLengthLimit is maximum allowed attribute value size */ + attributeValueLengthLimit?: number; + + /** attributeCountLimit is number of attributes per LogRecord */ + attributeCountLimit?: number; +} + +export interface LoggerConfig { + loggerSharedState: LoggerSharedState; + instrumentationScope: InstrumentationScope; + includeTraceContext: boolean; +} + +/** Interface configuration for a buffer. */ +export interface BufferConfig { + /** The maximum batch size of every export. It must be smaller or equal to + * maxQueueSize. The default value is 512. */ + maxExportBatchSize?: number; + + /** The delay interval in milliseconds between two consecutive exports. + * The default value is 5000ms. */ + scheduledDelayMillis?: number; + + /** How long the export can run before it is cancelled. + * The default value is 30000ms */ + exportTimeoutMillis?: number; + + /** The maximum queue size. After the size is reached log records are dropped. + * The default value is 2048. */ + maxQueueSize?: number; +} + +export interface BatchLogRecordProcessorBrowserConfig extends BufferConfig { + /** Disable flush when a user navigates to a new page, closes the tab or the browser, or, + * on mobile, switches to a different app. Auto flush is enabled by default. */ + disableAutoFlushOnDocumentHide?: boolean; +} diff --git a/experimental/packages/sdk-logs/tsconfig.esm.json b/experimental/packages/sdk-logs/tsconfig.esm.json new file mode 100644 index 0000000000..54150ddebe --- /dev/null +++ b/experimental/packages/sdk-logs/tsconfig.esm.json @@ -0,0 +1,25 @@ +{ + "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": "../api-logs" + } + ] +} diff --git a/experimental/packages/sdk-logs/tsconfig.esnext.json b/experimental/packages/sdk-logs/tsconfig.esnext.json new file mode 100644 index 0000000000..8cdb32ae00 --- /dev/null +++ b/experimental/packages/sdk-logs/tsconfig.esnext.json @@ -0,0 +1,25 @@ +{ + "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": "../api-logs" + } + ] +} diff --git a/experimental/packages/sdk-logs/tsconfig.json b/experimental/packages/sdk-logs/tsconfig.json new file mode 100644 index 0000000000..25205b8cf7 --- /dev/null +++ b/experimental/packages/sdk-logs/tsconfig.json @@ -0,0 +1,25 @@ +{ + "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": "../api-logs" + } + ] +} diff --git a/lerna.json b/lerna.json index d97ab02a62..57ea824aec 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,7 @@ { "version": "independent", "npmClient": "npm", + "useNx": false, "packages": [ "api", "packages/*", diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 247444bf58..a07b877244 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -38,6 +38,7 @@ export * from './trace/TraceState'; export * from './utils/environment'; export * from './utils/merge'; export * from './utils/sampling'; +export * from './utils/timeout'; export * from './utils/url'; export * from './utils/wrap'; export * from './utils/callback'; diff --git a/packages/opentelemetry-core/src/utils/timeout.ts b/packages/opentelemetry-core/src/utils/timeout.ts new file mode 100644 index 0000000000..adf8bf6b4a --- /dev/null +++ b/packages/opentelemetry-core/src/utils/timeout.ts @@ -0,0 +1,64 @@ +/* + * 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. + */ + +/** + * Error that is thrown on timeouts. + */ +export class TimeoutError extends Error { + constructor(message?: string) { + super(message); + + // manually adjust prototype to retain `instanceof` functionality when targeting ES5, see: + // https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, TimeoutError.prototype); + } +} + +/** + * Adds a timeout to a promise and rejects if the specified timeout has elapsed. Also rejects if the specified promise + * rejects, and resolves if the specified promise resolves. + * + *

NOTE: this operation will continue even after it throws a {@link TimeoutError}. + * + * @param promise promise to use with timeout. + * @param timeout the timeout in milliseconds until the returned promise is rejected. + */ +export function callWithTimeout( + promise: Promise, + timeout: number +): Promise { + let timeoutHandle: ReturnType; + + const timeoutPromise = new Promise(function timeoutFunction( + _resolve, + reject + ) { + timeoutHandle = setTimeout(function timeoutHandler() { + reject(new TimeoutError('Operation timed out.')); + }, timeout); + }); + + return Promise.race([promise, timeoutPromise]).then( + result => { + clearTimeout(timeoutHandle); + return result; + }, + reason => { + clearTimeout(timeoutHandle); + throw reason; + } + ); +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 3c3cb876dd..73e55365fe 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -32,6 +32,9 @@ { "path": "experimental/packages/otlp-transformer/tsconfig.esm.json" }, + { + "path": "experimental/packages/sdk-logs/tsconfig.esm.json" + }, { "path": "packages/opentelemetry-context-zone/tsconfig.esm.json" }, diff --git a/tsconfig.esnext.json b/tsconfig.esnext.json index 63c1e27aa4..c2bed9043c 100644 --- a/tsconfig.esnext.json +++ b/tsconfig.esnext.json @@ -32,6 +32,9 @@ { "path": "experimental/packages/otlp-transformer/tsconfig.esnext.json" }, + { + "path": "experimental/packages/sdk-logs/tsconfig.esnext.json" + }, { "path": "packages/opentelemetry-context-zone/tsconfig.esnext.json" }, diff --git a/tsconfig.json b/tsconfig.json index 347ccca34d..66a6a0dfdf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,6 +24,7 @@ "experimental/packages/otlp-grpc-exporter-base", "experimental/packages/otlp-proto-exporter-base", "experimental/packages/otlp-transformer", + "experimental/packages/sdk-logs", "packages/opentelemetry-context-async-hooks", "packages/opentelemetry-context-zone", "packages/opentelemetry-context-zone-peer-dep", @@ -111,6 +112,9 @@ { "path": "experimental/packages/otlp-transformer" }, + { + "path": "experimental/packages/sdk-logs" + }, { "path": "packages/opentelemetry-context-async-hooks" }, From 0a8910d633950478588facdb750168bd32534590 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 17 Jan 2023 18:54:15 +0800 Subject: [PATCH 02/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/package.json | 6 ++-- .../packages/sdk-logs/src/LogRecord.ts | 32 ++++++------------- experimental/packages/sdk-logs/src/Logger.ts | 4 +++ .../packages/sdk-logs/src/LoggerProvider.ts | 3 +- experimental/packages/sdk-logs/src/index.ts | 2 -- experimental/packages/sdk-logs/src/types.ts | 1 - 6 files changed, 18 insertions(+), 30 deletions(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 6467e7ba6e..f53bc023ba 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -90,8 +90,8 @@ "@opentelemetry/api": ">=1.3.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.8.0", - "@opentelemetry/resources": "1.8.0", - "@opentelemetry/api-logs": "0.34.0" + "@opentelemetry/core": "1.9.0", + "@opentelemetry/resources": "1.9.0", + "@opentelemetry/api-logs": "0.35.0" } } diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 03f0919bac..ddba89df29 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -48,32 +48,20 @@ export class LogRecord implements ReadableLogRecord { logRecord: logsAPI.LogRecord ) { const { - time = hrTime(), - observedTime, - // if includeTraceContext is true, Gets the current trace context by default - context = this.config.includeTraceContext - ? api.context.active() - : undefined, + timestamp = hrTime(), severityNumber, severityText, body, attributes = {}, + spanId, + traceFlags, + traceId, } = logRecord; - this.time = timeInputToHrTime(time); - this.observedTime = - observedTime === undefined - ? observedTime - : timeInputToHrTime(observedTime); - - if (context) { - const spanContext = api.trace.getSpanContext(context); - if (spanContext && api.isSpanContextValid(spanContext)) { - this.spanId = spanContext.spanId; - this.traceId = spanContext.traceId; - this.traceFlags = spanContext.traceFlags; - } - } + this.time = timeInputToHrTime(timestamp); + this.spanId = spanId; + this.traceId = traceId; + this.traceFlags = traceFlags; this.severityNumber = severityNumber; this.severityText = severityText; this.body = body; @@ -94,7 +82,7 @@ export class LogRecord implements ReadableLogRecord { this.config.loggerSharedState.activeProcessor.onEmit(this); } - private setAttribute(key: string, value?: AttributeValue): LogRecord { + public setAttribute(key: string, value?: AttributeValue): LogRecord { if (value === null || this._isLogRecordEmitted()) { return this; } @@ -117,7 +105,7 @@ export class LogRecord implements ReadableLogRecord { return this; } - private setAttributes(attributes: Attributes): LogRecord { + public setAttributes(attributes: Attributes): LogRecord { for (const [k, v] of Object.entries(attributes)) { this.setAttribute(k, v); } diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index e8fbe75a55..7590921b8f 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -22,6 +22,10 @@ import { LogRecord } from './LogRecord'; export class Logger implements logsAPI.Logger { constructor(private readonly config: LoggerConfig) {} + public emitEvent(event: logsAPI.LogEvent): void { + new LogRecord(this.config, event).emit(); + } + public emitLogRecord(logRecord: logsAPI.LogRecord): void { new LogRecord(this.config, logRecord).emit(); } diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 1fb7d47a33..833a93e877 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -53,7 +53,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { version?: string, options?: logsAPI.LoggerOptions ): Logger { - const { schemaUrl = '', includeTraceContext = true } = options || {}; + const { schemaUrl = '' } = options || {}; const key = `${name}@${version || ''}:${schemaUrl}`; if (!this._loggers.has(key)) { this._loggers.set( @@ -61,7 +61,6 @@ export class LoggerProvider implements logsAPI.LoggerProvider { new Logger({ loggerSharedState: this._loggerSharedState, instrumentationScope: { name, version, schemaUrl }, - includeTraceContext, }) ); } diff --git a/experimental/packages/sdk-logs/src/index.ts b/experimental/packages/sdk-logs/src/index.ts index 35c31c3433..62acee26ba 100644 --- a/experimental/packages/sdk-logs/src/index.ts +++ b/experimental/packages/sdk-logs/src/index.ts @@ -16,9 +16,7 @@ export * from "./types"; export * from "./LoggerProvider"; -export * from "./EventLoggerProvider"; export * from "./Logger"; -export * from "./EventLogger"; export * from "./LogRecord"; export * from "./LogRecordProcessor"; export * from "./export/ReadableLogRecord"; diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index 7ca29bdb47..0011178cf8 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -44,7 +44,6 @@ export interface LogRecordLimits { export interface LoggerConfig { loggerSharedState: LoggerSharedState; instrumentationScope: InstrumentationScope; - includeTraceContext: boolean; } /** Interface configuration for a buffer. */ From f25e7cf54073fdc538fc02ae762f71542a4a1c60 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 17 Jan 2023 22:13:51 +0800 Subject: [PATCH 03/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/package.json | 2 +- ...{semantic-conventions.ts => Attributes.ts} | 0 .../packages/sdk-logs/src/LogRecord.ts | 22 +++--- experimental/packages/sdk-logs/src/Logger.ts | 10 ++- .../packages/sdk-logs/src/LoggerProvider.ts | 42 +++++------ .../sdk-logs/src/LoggerSharedState.ts | 2 - .../sdk-logs/src/MultiLogRecordProcessor.ts | 16 +++-- .../src/export/BatchLogRecordProcessor.ts | 72 ++++++++++++++----- .../src/export/ConsoleLogRecordExporter.ts | 26 +++---- .../src/export/InMemoryLogRecordExporter.ts | 27 +++++-- .../sdk-logs/src/export/LogRecordExporter.ts | 9 ++- .../sdk-logs/src/export/ReadableLogRecord.ts | 1 - .../src/export/SimpleLogRecordProcessor.ts | 33 +++++++-- experimental/packages/sdk-logs/src/types.ts | 8 ++- .../src/utils/environment.ts | 4 ++ 15 files changed, 176 insertions(+), 98 deletions(-) rename experimental/packages/sdk-logs/src/{semantic-conventions.ts => Attributes.ts} (100%) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index f53bc023ba..2dce2849dd 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-logs", - "version": "0.34.0", + "version": "0.35.0", "publishConfig": { "access": "public" }, diff --git a/experimental/packages/sdk-logs/src/semantic-conventions.ts b/experimental/packages/sdk-logs/src/Attributes.ts similarity index 100% rename from experimental/packages/sdk-logs/src/semantic-conventions.ts rename to experimental/packages/sdk-logs/src/Attributes.ts diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index ddba89df29..b48e4e0f89 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -24,9 +24,9 @@ import { isAttributeValue, } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; -import { ReadableLogRecord } from './export/ReadableLogRecord'; -import { LoggerConfig } from './types'; +import type { Resource } from '@opentelemetry/resources'; +import type { ReadableLogRecord } from './export/ReadableLogRecord'; +import type { LoggerConfig } from './types'; export class LogRecord implements ReadableLogRecord { readonly time: api.HrTime; @@ -57,29 +57,25 @@ export class LogRecord implements ReadableLogRecord { traceFlags, traceId, } = logRecord; - this.time = timeInputToHrTime(timestamp); + this.time = timeInputToHrTime(timestamp); this.spanId = spanId; this.traceId = traceId; this.traceFlags = traceFlags; this.severityNumber = severityNumber; this.severityText = severityText; this.body = body; - this.resource = this.config.loggerSharedState.resource; + this.resource = this.config.resource; this.instrumentationScope = this.config.instrumentationScope; this.setAttributes(attributes); } public emit(): void { - if (this.config.loggerSharedState.shutdownOnceFeature.isCalled) { - api.diag.warn('can not emit, it is already shutdown'); - return; - } if (this._isLogRecordEmitted()) { return; } this._isEmitted = true; - this.config.loggerSharedState.activeProcessor.onEmit(this); + this.config.activeProcessor.onEmit(this); } public setAttribute(key: string, value?: AttributeValue): LogRecord { @@ -96,7 +92,7 @@ export class LogRecord implements ReadableLogRecord { } if ( Object.keys(this.attributes).length >= - this.config.loggerSharedState.logRecordLimits.attributeCountLimit! && + this.config.logRecordLimits.attributeCountLimit! && !Object.prototype.hasOwnProperty.call(this.attributes, key) ) { return this; @@ -120,9 +116,7 @@ export class LogRecord implements ReadableLogRecord { } private _truncateToSize(value: AttributeValue): AttributeValue { - const limit = - this.config.loggerSharedState.logRecordLimits.attributeValueLengthLimit || - 0; + const limit = this.config.logRecordLimits.attributeValueLengthLimit || 0; // Check limit if (limit <= 0) { // Negative values are invalid, so do not truncate diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index 7590921b8f..b6b107eae4 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -18,12 +18,20 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import type { LoggerConfig } from './types'; import { LogRecord } from './LogRecord'; +import { EVENT_LOGS_ATTRIBUTES } from './Attributes'; +import { DEFAULT_EVENT_DOMAIN } from './config'; export class Logger implements logsAPI.Logger { constructor(private readonly config: LoggerConfig) {} public emitEvent(event: logsAPI.LogEvent): void { - new LogRecord(this.config, event).emit(); + new LogRecord(this.config, event) + .setAttributes({ + [EVENT_LOGS_ATTRIBUTES.name]: event.name, + [EVENT_LOGS_ATTRIBUTES.domain]: + event.domain ?? this.config.eventDomain ?? DEFAULT_EVENT_DOMAIN, + }) + .emit(); } public emitLogRecord(logRecord: logsAPI.LogRecord): void { diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 833a93e877..a1314fdbe8 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -16,19 +16,19 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import { Resource } from '@opentelemetry/resources'; -import { BindOnceFuture, merge } from '@opentelemetry/core'; +import { merge } from '@opentelemetry/core'; -import type { LoggerProviderConfig } from './types'; +import type { LoggerProviderConfig, LogRecordLimits } from './types'; import type { LogRecordProcessor } from './LogRecordProcessor'; -import type { LoggerSharedState } from './LoggerSharedState'; import { Logger } from './Logger'; import { loadDefaultConfig } from './config'; import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; export class LoggerProvider implements logsAPI.LoggerProvider { - private readonly _loggerSharedState: LoggerSharedState; private readonly _loggers: Map = new Map(); - private _shutdownOnceFeature: BindOnceFuture; + private readonly _resource: Resource; + private readonly _logRecordLimits: LogRecordLimits; + private readonly _activeProcessor: MultiLogRecordProcessor; constructor(config: LoggerProviderConfig = {}) { const { resource, logRecordLimits, forceFlushTimeoutMillis } = merge( @@ -36,13 +36,11 @@ export class LoggerProvider implements logsAPI.LoggerProvider { loadDefaultConfig(), config ); - this._shutdownOnceFeature = new BindOnceFuture(this._showdownFeature, this); - this._loggerSharedState = { - resource: Resource.default().merge(resource ?? Resource.empty()), - activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), - shutdownOnceFeature: this._shutdownOnceFeature, - logRecordLimits, - }; + this._resource = Resource.default().merge(resource ?? Resource.empty()); + this._activeProcessor = new MultiLogRecordProcessor( + forceFlushTimeoutMillis + ); + this._logRecordLimits = logRecordLimits; } /** @@ -53,13 +51,16 @@ export class LoggerProvider implements logsAPI.LoggerProvider { version?: string, options?: logsAPI.LoggerOptions ): Logger { - const { schemaUrl = '' } = options || {}; + const { schemaUrl = '', eventDomain } = options || {}; const key = `${name}@${version || ''}:${schemaUrl}`; if (!this._loggers.has(key)) { this._loggers.set( key, new Logger({ - loggerSharedState: this._loggerSharedState, + eventDomain, + resource: this._resource, + logRecordLimits: this._logRecordLimits, + activeProcessor: this._activeProcessor, instrumentationScope: { name, version, schemaUrl }, }) ); @@ -72,7 +73,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { * @param processor the new LogRecordProcessor to be added. */ public addLogRecordProcessor(processor: LogRecordProcessor) { - this._loggerSharedState.activeProcessor.addLogRecordProcessor(processor); + this._activeProcessor.addLogRecordProcessor(processor); } /** @@ -81,10 +82,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { * Returns a promise which is resolved when all flushes are complete. */ public forceFlush(): Promise { - if (this._shutdownOnceFeature.isCalled) { - return Promise.reject('can not flush, it is already shutdown'); - } - return this._loggerSharedState.activeProcessor.forceFlush(); + return this._activeProcessor.forceFlush(); } /** @@ -94,10 +92,6 @@ export class LoggerProvider implements logsAPI.LoggerProvider { * Returns a promise which is resolved when all flushes are complete. */ public shutdown(): Promise { - return this._shutdownOnceFeature.call(); - } - - private _showdownFeature(): Promise { - return this._loggerSharedState.activeProcessor.shutdown(); + return this._activeProcessor.shutdown(); } } diff --git a/experimental/packages/sdk-logs/src/LoggerSharedState.ts b/experimental/packages/sdk-logs/src/LoggerSharedState.ts index e36ddd05c4..6ffd1d1b7c 100644 --- a/experimental/packages/sdk-logs/src/LoggerSharedState.ts +++ b/experimental/packages/sdk-logs/src/LoggerSharedState.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import type { BindOnceFuture } from '@opentelemetry/core'; import type { Resource } from '@opentelemetry/resources'; import type { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; @@ -24,5 +23,4 @@ export interface LoggerSharedState { readonly activeProcessor: MultiLogRecordProcessor; readonly resource: Resource; readonly logRecordLimits: LogRecordLimits; - readonly shutdownOnceFeature: BindOnceFuture; } diff --git a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts index cde9ba49de..b46cd31b4c 100644 --- a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { callWithTimeout } from "@opentelemetry/core"; +import { callWithTimeout } from '@opentelemetry/core'; -import type { LogRecordProcessor } from "./LogRecordProcessor"; -import type { ReadableLogRecord } from "./export/ReadableLogRecord"; +import type { LogRecordProcessor } from './LogRecordProcessor'; +import type { ReadableLogRecord } from './export/ReadableLogRecord'; /** * Implementation of the {@link LogRecordProcessor} that simply forwards all @@ -34,14 +34,18 @@ export class MultiLogRecordProcessor implements LogRecordProcessor { public async forceFlush(): Promise { const timeout = this._forceFlushTimeoutMillis; - await Promise.all(this._processors.map((processor) => callWithTimeout(processor.forceFlush(), timeout))); + await Promise.all( + this._processors.map(processor => + callWithTimeout(processor.forceFlush(), timeout) + ) + ); } public onEmit(logRecord: ReadableLogRecord): void { - this._processors.forEach((processors) => processors.onEmit(logRecord)); + this._processors.forEach(processors => processors.onEmit(logRecord)); } public async shutdown(): Promise { - await Promise.all(this._processors.map((processor) => processor.shutdown())); + await Promise.all(this._processors.map(processor => processor.shutdown())); } } diff --git a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts index aa4e96dc06..95aacdb58b 100644 --- a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts @@ -14,16 +14,25 @@ * limitations under the License. */ -import type { ExportResult } from "@opentelemetry/core"; -import { diag } from "@opentelemetry/api"; -import { ExportResultCode, getEnv, globalErrorHandler, unrefTimer, callWithTimeout } from "@opentelemetry/core"; - -import type { BufferConfig } from "../types"; -import type { ReadableLogRecord } from "./ReadableLogRecord"; -import type { LogRecordExporter } from "./LogRecordExporter"; -import type { LogRecordProcessor } from "./../LogRecordProcessor"; - -export abstract class BatchLogRecordProcessorBase implements LogRecordProcessor { +import type { ExportResult } from '@opentelemetry/core'; +import { diag } from '@opentelemetry/api'; +import { + ExportResultCode, + getEnv, + globalErrorHandler, + unrefTimer, + callWithTimeout, + BindOnceFuture, +} from '@opentelemetry/core'; + +import type { BufferConfig } from '../types'; +import type { ReadableLogRecord } from './ReadableLogRecord'; +import type { LogRecordExporter } from './LogRecordExporter'; +import type { LogRecordProcessor } from './../LogRecordProcessor'; + +export abstract class BatchLogRecordProcessorBase + implements LogRecordProcessor +{ private readonly _maxExportBatchSize: number; private readonly _maxQueueSize: number; private readonly _scheduledDelayMillis: number; @@ -31,31 +40,47 @@ export abstract class BatchLogRecordProcessorBase implem private _finishedLogRecords: ReadableLogRecord[] = []; private _timer: NodeJS.Timeout | undefined; + private _shutdownOnce: BindOnceFuture; constructor(private readonly _exporter: LogRecordExporter, config?: T) { const env = getEnv(); - this._maxExportBatchSize = config?.maxExportBatchSize ?? env.OTEL_BSP_MAX_EXPORT_BATCH_SIZE; + this._maxExportBatchSize = + config?.maxExportBatchSize ?? env.OTEL_BSP_MAX_EXPORT_BATCH_SIZE; this._maxQueueSize = config?.maxQueueSize ?? env.OTEL_BSP_MAX_QUEUE_SIZE; - this._scheduledDelayMillis = config?.scheduledDelayMillis ?? env.OTEL_BSP_SCHEDULE_DELAY; - this._exportTimeoutMillis = config?.exportTimeoutMillis ?? env.OTEL_BSP_EXPORT_TIMEOUT; + this._scheduledDelayMillis = + config?.scheduledDelayMillis ?? env.OTEL_BSP_SCHEDULE_DELAY; + this._exportTimeoutMillis = + config?.exportTimeoutMillis ?? env.OTEL_BSP_EXPORT_TIMEOUT; + + this._shutdownOnce = new BindOnceFuture(this._shutdown, this); if (this._maxExportBatchSize > this._maxQueueSize) { diag.warn( - "BatchLogRecordProcessor: maxExportBatchSize must be smaller or equal to maxQueueSize, setting maxExportBatchSize to match maxQueueSize" + 'BatchLogRecordProcessor: maxExportBatchSize must be smaller or equal to maxQueueSize, setting maxExportBatchSize to match maxQueueSize' ); this._maxExportBatchSize = this._maxQueueSize; } } public onEmit(logRecord: ReadableLogRecord): void { + if (this._shutdownOnce.isCalled) { + return; + } this._addToBuffer(logRecord); } public forceFlush(): Promise { + if (this._shutdownOnce.isCalled) { + return this._shutdownOnce.promise; + } return this._flushAll(); } - public async shutdown(): Promise { + public shutdown(): Promise { + return this._shutdownOnce.call(); + } + + private async _shutdown(): Promise { this.onShutdown(); await this._flushAll(); await this._exporter.shutdown(); @@ -78,7 +103,9 @@ export abstract class BatchLogRecordProcessorBase implem private _flushAll(): Promise { return new Promise((resolve, reject) => { const promises = []; - const batchCount = Math.ceil(this._finishedLogRecords.length / this._maxExportBatchSize); + const batchCount = Math.ceil( + this._finishedLogRecords.length / this._maxExportBatchSize + ); for (let i = 0; i < batchCount; i++) { promises.push(this._flushOneBatch()); } @@ -97,7 +124,9 @@ export abstract class BatchLogRecordProcessorBase implem } return new Promise((resolve, reject) => { callWithTimeout( - this._export(this._finishedLogRecords.splice(0, this._maxExportBatchSize)), + this._export( + this._finishedLogRecords.splice(0, this._maxExportBatchSize) + ), this._exportTimeoutMillis ) .then(() => resolve()) @@ -117,7 +146,7 @@ export abstract class BatchLogRecordProcessorBase implem this._maybeStartTimer(); } }) - .catch((e) => { + .catch(e => { globalErrorHandler(e); }); }, this._scheduledDelayMillis); @@ -135,7 +164,12 @@ export abstract class BatchLogRecordProcessorBase implem return new Promise((resolve, reject) => { this._exporter.export(logRecords, (res: ExportResult) => { if (res.code !== ExportResultCode.SUCCESS) { - reject(res.error ?? new Error(`BatchLogRecordProcessorBase: log record export failed (status ${res})`)); + reject( + res.error ?? + new Error( + `BatchLogRecordProcessorBase: log record export failed (status ${res})` + ) + ); return; } resolve(res); diff --git a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts index 6a21e0850d..7e1a40679f 100644 --- a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts +++ b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts @@ -44,15 +44,6 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { return Promise.resolve(); } - private _sendLogRecords( - logRecords: ReadableLogRecord[] - ): Promise { - for (const logRecord of logRecords) { - console.dir(this._exportInfo(logRecord), { depth: 3 }); - } - return Promise.resolve({ code: ExportResultCode.SUCCESS }); - } - /** * converts logRecord info into more readable format * @param span @@ -60,9 +51,6 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { private _exportInfo(logRecord: ReadableLogRecord) { return { timestamp: hrTimeToMicroseconds(logRecord.time), - observedTimestamp: logRecord.observedTime - ? hrTimeToMicroseconds(logRecord.observedTime) - : undefined, traceId: logRecord.traceId, spanId: logRecord.spanId, traceFlags: logRecord.traceFlags, @@ -72,4 +60,18 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { attributes: logRecord.attributes, }; } + + /** + * Showing logs in console + * @param logRecords + * @param done + */ + private _sendLogRecords( + logRecords: ReadableLogRecord[] + ): Promise { + for (const logRecord of logRecords) { + console.dir(this._exportInfo(logRecord), { depth: 3 }); + } + return Promise.resolve({ code: ExportResultCode.SUCCESS }); + } } diff --git a/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts index 03b0cc6da4..526fd8ddd1 100644 --- a/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts +++ b/experimental/packages/sdk-logs/src/export/InMemoryLogRecordExporter.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import type { ExportResult } from "@opentelemetry/core"; -import { ExportResultCode } from "@opentelemetry/core"; +import type { ExportResult } from '@opentelemetry/core'; +import { ExportResultCode } from '@opentelemetry/core'; -import type { ReadableLogRecord } from "./ReadableLogRecord"; -import type { LogRecordExporter } from "./LogRecordExporter"; +import type { ReadableLogRecord } from './ReadableLogRecord'; +import type { LogRecordExporter } from './LogRecordExporter'; /** * This class can be used for testing purposes. It stores the exported LogRecords @@ -28,12 +28,29 @@ import type { LogRecordExporter } from "./LogRecordExporter"; export class InMemoryLogRecordExporter implements LogRecordExporter { private _finishedLogRecords: ReadableLogRecord[] = []; - public export(logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void) { + /** + * Indicates if the exporter has been "shutdown." + * When false, exported log records will not be stored in-memory. + */ + protected _stopped = false; + + public export( + logs: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void + ) { + if (this._stopped) { + return resultCallback({ + code: ExportResultCode.FAILED, + error: new Error('Exporter has been stopped'), + }); + } + this._finishedLogRecords.push(...logs); resultCallback({ code: ExportResultCode.SUCCESS }); } public shutdown(): Promise { + this._stopped = true; this.reset(); return Promise.resolve(); } diff --git a/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts index 0738f46c44..4fecb2a8c9 100644 --- a/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts +++ b/experimental/packages/sdk-logs/src/export/LogRecordExporter.ts @@ -14,16 +14,19 @@ * limitations under the License. */ -import type { ExportResult } from "@opentelemetry/core"; +import type { ExportResult } from '@opentelemetry/core'; -import type { ReadableLogRecord } from "./ReadableLogRecord"; +import type { ReadableLogRecord } from './ReadableLogRecord'; export interface LogRecordExporter { /** * Called to export {@link ReadableLogRecord}s. * @param logs the list of sampled LogRecords to be exported. */ - export(logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void): void; + export( + logs: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void + ): void; /** Stops the exporter. */ shutdown(): Promise; diff --git a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts index 25163878fe..b211d521c0 100644 --- a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts +++ b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts @@ -21,7 +21,6 @@ import type { SeverityNumber } from '@opentelemetry/api-logs'; export interface ReadableLogRecord { readonly time: HrTime; - readonly observedTime?: HrTime; readonly traceId?: string; readonly spanId?: string; readonly traceFlags?: number; diff --git a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts index 77d8f3b125..cfe8aea651 100644 --- a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts @@ -14,21 +14,36 @@ * limitations under the License. */ -import type { ExportResult } from "@opentelemetry/core"; -import { ExportResultCode, globalErrorHandler } from "@opentelemetry/core"; +import type { ExportResult } from '@opentelemetry/core'; +import { + BindOnceFuture, + ExportResultCode, + globalErrorHandler, +} from '@opentelemetry/core'; -import type { LogRecordExporter } from "./LogRecordExporter"; -import type { LogRecordProcessor } from "../LogRecordProcessor"; -import type { ReadableLogRecord } from "./ReadableLogRecord"; +import type { LogRecordExporter } from './LogRecordExporter'; +import type { LogRecordProcessor } from '../LogRecordProcessor'; +import type { ReadableLogRecord } from './ReadableLogRecord'; export class SimpleLogRecordProcessor implements LogRecordProcessor { - constructor(private readonly _exporter: LogRecordExporter) {} + private _shutdownOnce: BindOnceFuture; + + constructor(private readonly _exporter: LogRecordExporter) { + this._shutdownOnce = new BindOnceFuture(this._shutdown, this); + } public onEmit(logRecord: ReadableLogRecord): void { + if (this._shutdownOnce.isCalled) { + return; + } + this._exporter.export([logRecord], (res: ExportResult) => { if (res.code !== ExportResultCode.SUCCESS) { globalErrorHandler( - res.error ?? new Error(`SimpleLogRecordProcessor: log record export failed (status ${res})`) + res.error ?? + new Error( + `SimpleLogRecordProcessor: log record export failed (status ${res})` + ) ); return; } @@ -41,6 +56,10 @@ export class SimpleLogRecordProcessor implements LogRecordProcessor { } public shutdown(): Promise { + return this._shutdownOnce.call(); + } + + private _shutdown(): Promise { return this._exporter.shutdown(); } } diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index 0011178cf8..e1c0b144b7 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -16,8 +16,7 @@ import type { Resource } from '@opentelemetry/resources'; import type { InstrumentationScope } from '@opentelemetry/core'; - -import type { LoggerSharedState } from './LoggerSharedState'; +import type { LogRecordProcessor } from './LogRecordProcessor'; export interface LoggerProviderConfig { /** Resource associated with trace telemetry */ @@ -42,7 +41,10 @@ export interface LogRecordLimits { } export interface LoggerConfig { - loggerSharedState: LoggerSharedState; + eventDomain?: string; + activeProcessor: LogRecordProcessor; + resource: Resource; + logRecordLimits: LogRecordLimits; instrumentationScope: InstrumentationScope; } diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index 01da17f293..0b1a3c5f0f 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -45,6 +45,8 @@ const ENVIRONMENT_NUMBERS_KEYS = [ 'OTEL_ATTRIBUTE_COUNT_LIMIT', 'OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT', 'OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT', + 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT', + 'OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT', 'OTEL_SPAN_EVENT_COUNT_LIMIT', 'OTEL_SPAN_LINK_COUNT_LIMIT', 'OTEL_EXPORTER_OTLP_TIMEOUT', @@ -170,6 +172,8 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, + OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT , + OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT: 128, OTEL_SPAN_LINK_COUNT_LIMIT: 128, OTEL_TRACES_EXPORTER: '', From b50422e0f554be5d0d7d2d04b6bab4d9aacb80a7 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 18 Jan 2023 22:17:11 +0800 Subject: [PATCH 04/44] feat(sdk-logs): sdk-logs init --- .../packages/sdk-logs/src/LogRecord.ts | 15 +- experimental/packages/sdk-logs/src/Logger.ts | 14 +- .../export/BatchLogRecordProcessor.test.ts | 120 ++++++ .../sdk-logs/test/common/LogRecord.test.ts | 243 +++++++++++++ .../sdk-logs/test/common/Logger.test.ts | 117 ++++++ .../test/common/LoggerProvider.test.ts | 159 ++++++++ .../common/MultiLogRecordProcessor.test.ts | 128 +++++++ .../export/BatchLogRecordProcessor.test.ts | 344 ++++++++++++++++++ .../export/ConsoleLogRecordExporter.test.ts | 50 +++ .../export/InMemoryLogRecordExporter.test.ts | 91 +++++ .../export/SimpleLogRecordProcessor.test.ts | 79 ++++ .../packages/sdk-logs/test/common/utils.ts | 33 ++ 12 files changed, 1377 insertions(+), 16 deletions(-) create mode 100644 experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/LogRecord.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/Logger.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/utils.ts diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index b48e4e0f89..6f84ece21b 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -30,7 +30,6 @@ import type { LoggerConfig } from './types'; export class LogRecord implements ReadableLogRecord { readonly time: api.HrTime; - readonly observedTime?: api.HrTime; readonly traceId?: string; readonly spanId?: string; readonly traceFlags?: number; @@ -78,34 +77,32 @@ export class LogRecord implements ReadableLogRecord { this.config.activeProcessor.onEmit(this); } - public setAttribute(key: string, value?: AttributeValue): LogRecord { + public setAttribute(key: string, value?: AttributeValue) { if (value === null || this._isLogRecordEmitted()) { - return this; + return; } if (key.length === 0) { api.diag.warn(`Invalid attribute key: ${key}`); - return this; + return; } if (!isAttributeValue(value)) { api.diag.warn(`Invalid attribute value set for key: ${key}`); - return this; + return; } if ( Object.keys(this.attributes).length >= this.config.logRecordLimits.attributeCountLimit! && !Object.prototype.hasOwnProperty.call(this.attributes, key) ) { - return this; + return; } this.attributes[key] = this._truncateToSize(value); - return this; } - public setAttributes(attributes: Attributes): LogRecord { + public setAttributes(attributes: Attributes) { for (const [k, v] of Object.entries(attributes)) { this.setAttribute(k, v); } - return this; } private _isLogRecordEmitted(): boolean { diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index b6b107eae4..bdd0a9fa34 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -25,13 +25,13 @@ export class Logger implements logsAPI.Logger { constructor(private readonly config: LoggerConfig) {} public emitEvent(event: logsAPI.LogEvent): void { - new LogRecord(this.config, event) - .setAttributes({ - [EVENT_LOGS_ATTRIBUTES.name]: event.name, - [EVENT_LOGS_ATTRIBUTES.domain]: - event.domain ?? this.config.eventDomain ?? DEFAULT_EVENT_DOMAIN, - }) - .emit(); + const logRecord = new LogRecord(this.config, event); + logRecord.setAttributes({ + [EVENT_LOGS_ATTRIBUTES.name]: event.name, + [EVENT_LOGS_ATTRIBUTES.domain]: + event.domain ?? this.config.eventDomain ?? DEFAULT_EVENT_DOMAIN, + }); + logRecord.emit(); } public emitLogRecord(logRecord: logsAPI.LogRecord): void { diff --git a/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts new file mode 100644 index 0000000000..05ffc546c9 --- /dev/null +++ b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { LogRecordExporter } from "../../../src"; +import { BatchLogRecordProcessor } from "../../../src/platform/browser/export/BatchLogRecordProcessor"; +import { InMemoryLogRecordExporter } from "./../../../src/export/InMemoryLogRecordExporter"; + +const describeDocument = typeof document === "object" ? describe : describe.skip; + +describeDocument("BatchLogRecordProcessor - web main context", () => { + let visibilityState: VisibilityState = "visible"; + let exporter: LogRecordExporter; + let processor: BatchLogRecordProcessor; + let forceFlushSpy: sinon.SinonStub; + let visibilityChangeEvent: Event; + let pageHideEvent: Event; + + beforeEach(() => { + sinon.replaceGetter(document, "visibilityState", () => visibilityState); + visibilityState = "visible"; + exporter = new InMemoryLogRecordExporter(); + processor = new BatchLogRecordProcessor(exporter, {}); + forceFlushSpy = sinon.stub(processor, "forceFlush"); + visibilityChangeEvent = new Event("visibilitychange"); + pageHideEvent = new Event("pagehide"); + }); + + afterEach(async () => { + sinon.restore(); + }); + + describe("when document becomes hidden", () => { + const testDocumentHide = (hideDocument: () => void) => { + it("should force flush log records", () => { + assert.strictEqual(forceFlushSpy.callCount, 0); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 1); + }); + + describe("AND shutdown has been called", () => { + it("should NOT force flush log records", async () => { + assert.strictEqual(forceFlushSpy.callCount, 0); + await processor.shutdown(); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 0); + }); + }); + + describe("AND disableAutoFlushOnDocumentHide configuration option", () => { + it("set to false should force flush log records", () => { + processor = new BatchLogRecordProcessor(exporter, { + disableAutoFlushOnDocumentHide: false, + }); + forceFlushSpy = sinon.stub(processor, "forceFlush"); + assert.strictEqual(forceFlushSpy.callCount, 0); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 1); + }); + + it("set to true should NOT force flush log records", () => { + processor = new BatchLogRecordProcessor(exporter, { + disableAutoFlushOnDocumentHide: true, + }); + forceFlushSpy = sinon.stub(processor, "forceFlush"); + assert.strictEqual(forceFlushSpy.callCount, 0); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 0); + }); + }); + }; + + describe("by the visibilitychange event", () => { + testDocumentHide(() => { + visibilityState = "hidden"; + document.dispatchEvent(visibilityChangeEvent); + }); + }); + + describe("by the pagehide event", () => { + testDocumentHide(() => { + document.dispatchEvent(pageHideEvent); + }); + }); + }); + + describe("when document becomes visible", () => { + it("should NOT force flush log records", () => { + assert.strictEqual(forceFlushSpy.callCount, 0); + document.dispatchEvent(visibilityChangeEvent); + assert.strictEqual(forceFlushSpy.callCount, 0); + }); + }); +}); + +describe("BatchLogRecordProcessor", () => { + it("without exception", async () => { + const exporter = new InMemoryLogRecordExporter(); + const logRecordProcessor = new BatchLogRecordProcessor(exporter); + assert.ok(logRecordProcessor instanceof BatchLogRecordProcessor); + + await logRecordProcessor.forceFlush(); + await logRecordProcessor.shutdown(); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts new file mode 100644 index 0000000000..f8e66a30ff --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts @@ -0,0 +1,243 @@ +/* + * 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 type { Attributes, AttributeValue } from '@opentelemetry/api'; +import { SeverityNumber } from '@opentelemetry/api-logs'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import type * as logsAPI from '@opentelemetry/api-logs'; +import { Resource } from '@opentelemetry/resources'; +import { InstrumentationScope, timeInputToHrTime } from '@opentelemetry/core'; + +import type { LogRecordLimits } from './../../src'; +import { LogRecord } from './../../src'; +import { loadDefaultConfig } from '../../src/config'; +import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; +import { invalidAttributes, validAttributes } from './utils'; + +const setup = ( + limits?: LogRecordLimits, + instrumentationScope?: InstrumentationScope, + data?: logsAPI.LogRecord +) => { + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const logRecord = new LogRecord( + { + eventDomain: 'eventDomain', + activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), + resource: Resource.default(), + logRecordLimits: { + attributeValueLengthLimit: + limits?.attributeValueLengthLimit ?? + logRecordLimits.attributeValueLengthLimit, + attributeCountLimit: + limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, + }, + instrumentationScope: instrumentationScope || { name: 'test' }, + }, + data || {} + ); + return { logRecord }; +}; + +describe('LogRecord', () => { + describe('constructor', () => { + it('should create an instance', () => { + const { logRecord } = setup(); + assert.ok(logRecord instanceof LogRecord); + }); + + it('should have a default timestamp', () => { + const { logRecord } = setup(); + const { time } = logRecord; + assert.ok(time !== undefined); + }); + + it('should create an instance with logRecord', () => { + const logRecordData = { + timestamp: new Date().getTime(), + severityNumber: SeverityNumber.DEBUG, + severityText: 'DEBUG', + body: 'this is a body', + attributes: { + name: 'zhangsan', + }, + traceId: 'trance id', + spanId: 'span id', + traceFlags: 1, + }; + const instrumentationScope = { + name: 'name', + }; + + const logRecord = setup( + undefined, + instrumentationScope, + logRecordData + ).logRecord; + assert.deepStrictEqual( + logRecord.time, + timeInputToHrTime(logRecordData.timestamp) + ); + assert.strictEqual( + logRecord.severityNumber, + logRecordData.severityNumber + ); + assert.strictEqual(logRecord.severityText, logRecordData.severityText); + assert.strictEqual(logRecord.body, logRecordData.body); + assert.deepStrictEqual(logRecord.attributes, logRecordData.attributes); + assert.strictEqual(logRecord.traceId, logRecordData.traceId); + assert.strictEqual(logRecord.spanId, logRecordData.spanId); + assert.strictEqual(logRecord.traceFlags, logRecordData.traceFlags); + assert.deepStrictEqual( + logRecord.instrumentationScope, + instrumentationScope + ); + }); + }); + + describe('setAttribute', () => { + describe('when default options set', () => { + it('should set an attribute', () => { + const { logRecord } = setup(); + for (const [k, v] of Object.entries(validAttributes)) { + logRecord.setAttribute(k, v); + } + for (const [k, v] of Object.entries(invalidAttributes)) { + logRecord.setAttribute(k, v as unknown as AttributeValue); + } + assert.deepStrictEqual(logRecord.attributes, validAttributes); + }); + + it('should be able to overwrite attributes', () => { + const { logRecord } = setup(); + logRecord.setAttribute('overwrite', 'initial value'); + logRecord.setAttribute('overwrite', 'overwritten value'); + assert.deepStrictEqual(logRecord.attributes, { + overwrite: 'overwritten value', + }); + }); + }); + + describe('when logRecordLimits options set', () => { + describe('when "attributeCountLimit" option defined', () => { + const { logRecord } = setup({ attributeCountLimit: 100 }); + for (let i = 0; i < 150; i++) { + logRecord.setAttribute(`foo${i}`, `bar${i}`); + } + + it('should remove / drop all remaining values after the number of values exceeds this limit', () => { + const { attributes } = logRecord; + assert.strictEqual(Object.keys(attributes).length, 100); + assert.strictEqual(attributes.foo0, 'bar0'); + assert.strictEqual(attributes.foo99, 'bar99'); + assert.strictEqual(attributes.foo149, undefined); + }); + }); + + describe('when "attributeValueLengthLimit" option defined', () => { + const { logRecord } = setup({ attributeValueLengthLimit: 5 }); + const { attributes } = logRecord; + + it('should truncate value which length exceeds this limit', () => { + logRecord.setAttribute('attr-with-more-length', 'abcdefgh'); + assert.strictEqual(attributes['attr-with-more-length'], 'abcde'); + }); + + it('should truncate value of arrays which exceeds this limit', () => { + logRecord.setAttribute('attr-array-of-strings', [ + 'abcdefgh', + 'abc', + 'abcde', + '', + ]); + logRecord.setAttribute('attr-array-of-bool', [true, false]); + assert.deepStrictEqual(attributes['attr-array-of-strings'], [ + 'abcde', + 'abc', + 'abcde', + '', + ]); + assert.deepStrictEqual(attributes['attr-array-of-bool'], [ + true, + false, + ]); + }); + + it('should not truncate value which length not exceeds this limit', () => { + logRecord.setAttribute('attr-with-less-length', 'abc'); + assert.strictEqual(attributes['attr-with-less-length'], 'abc'); + }); + + it('should return same value for non-string values', () => { + logRecord.setAttribute('attr-non-string', true); + assert.strictEqual(attributes['attr-non-string'], true); + }); + }); + + describe('when "attributeValueLengthLimit" option is invalid', () => { + const { logRecord } = setup({ attributeValueLengthLimit: -5 }); + const { attributes } = logRecord; + + it('should not truncate any value', () => { + logRecord.setAttribute('attr-not-truncate', 'abcdefgh'); + logRecord.setAttribute('attr-array-of-strings', [ + 'abcdefgh', + 'abc', + 'abcde', + ]); + assert.deepStrictEqual(attributes['attr-not-truncate'], 'abcdefgh'); + assert.deepStrictEqual(attributes['attr-array-of-strings'], [ + 'abcdefgh', + 'abc', + 'abcde', + ]); + }); + }); + }); + }); + + describe('setAttributes', () => { + it('should be able to set multiple attributes', () => { + const { logRecord } = setup(); + logRecord.setAttributes(validAttributes); + logRecord.setAttributes(invalidAttributes as unknown as Attributes); + assert.deepStrictEqual(logRecord.attributes, validAttributes); + }); + }); + + describe('emit', () => { + it('should be emit', () => { + const { logRecord } = setup(); + // @ts-expect-error + const callSpy = sinon.spy(logRecord.config.activeProcessor, 'onEmit'); + logRecord.emit(); + assert.ok(callSpy.called); + }); + + it('should be allow emit only once', () => { + const { logRecord } = setup(); + const callSpy = sinon.spy( + // @ts-expect-error + logRecord.config.activeProcessor, + 'onEmit' + ); + logRecord.emit(); + logRecord.emit(); + assert.ok(callSpy.callCount === 1); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts new file mode 100644 index 0000000000..eab7d357e1 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -0,0 +1,117 @@ +/* + * 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 { Resource } from '@opentelemetry/resources'; + +import { Logger, LogRecord } from '../../src'; +import { DEFAULT_EVENT_DOMAIN, loadDefaultConfig } from '../../src/config'; +import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; +import { EVENT_LOGS_ATTRIBUTES } from '../../src/Attributes'; + +const setup = (eventDomain?: string) => { + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const logger = new Logger({ + eventDomain, + // @ts-expect-error + loggerSharedState: { + activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), + resource: Resource.default(), + logRecordLimits, + }, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }); + return { logger }; +}; + +describe('Logger', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('constructor', () => { + it('should create an instance', () => { + const { logger } = setup(); + assert.ok(logger instanceof Logger); + }); + }); + + describe('emitLogRecord', () => { + it('should emit log record', () => { + const emitSpy = sinon.stub(LogRecord.prototype, 'emit'); + const { logger } = setup(); + logger.emitLogRecord({}); + assert.strictEqual(emitSpy.callCount, 1); + }); + }); + + describe('emitEvent', () => { + it('should emit log record with event attributes', () => { + const emitSpy = sinon.stub(LogRecord.prototype, 'emit'); + const setAttributesSpy = sinon.stub(LogRecord.prototype, 'setAttributes'); + + const eventName = 'test event name'; + const eventDomain = 'test event domain'; + + const { logger } = setup(eventDomain); + logger.emitEvent({ name: eventName }); + + const eventAttributes = { + [EVENT_LOGS_ATTRIBUTES.name]: eventName, + [EVENT_LOGS_ATTRIBUTES.domain]: eventDomain, + }; + + assert.strictEqual(emitSpy.callCount, 1); + assert.deepEqual(setAttributesSpy.secondCall.args[0], eventAttributes); + }); + + it('should have default eventDomain if not pass', () => { + sinon.stub(LogRecord.prototype, 'emit'); + const setAttributesSpy = sinon.stub(LogRecord.prototype, 'setAttributes'); + + const eventName = 'test event name'; + const { logger } = setup(); + logger.emitEvent({ name: eventName }); + + assert.strictEqual( + setAttributesSpy.secondCall.args[0][EVENT_LOGS_ATTRIBUTES.domain], + DEFAULT_EVENT_DOMAIN + ); + }); + + it('should use eventDomain if LogEvent has eventDomain', () => { + sinon.stub(LogRecord.prototype, 'emit'); + const setAttributesSpy = sinon.stub(LogRecord.prototype, 'setAttributes'); + + const eventName = 'test event name'; + const loggerEventDomain = 'test event domain in logger'; + const emitEventDomain = 'test event domain in emitEvent'; + + const { logger } = setup(loggerEventDomain); + logger.emitEvent({ name: eventName, domain: emitEventDomain }); + + assert.strictEqual( + setAttributesSpy.secondCall.args[0][EVENT_LOGS_ATTRIBUTES.domain], + emitEventDomain + ); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts new file mode 100644 index 0000000000..e548d75db1 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { Resource } from '@opentelemetry/resources'; + +import type { LogRecordProcessor } from './../../src/LogRecordProcessor'; +import { Logger, LoggerProvider } from '../../src'; +import { loadDefaultConfig } from '../../src/config'; +import { DEFAULT_EVENT_DOMAIN } from './../../src/config'; + +describe('LoggerProvider', () => { + afterEach(() => { + sinon.restore(); + }); + + describe('constructor', () => { + it('should contruct an instance', () => { + const provider = new LoggerProvider(); + assert.ok(provider instanceof LoggerProvider); + }); + + it('should have default resource if not pass', () => { + const provider = new LoggerProvider(); + // @ts-expect-error + const { _resource } = provider; + assert.deepStrictEqual(_resource, Resource.default()); + }); + + it('should have default logRecordLimits if not pass', () => { + const provider = new LoggerProvider(); + // @ts-expect-error + const { _logRecordLimits } = provider; + assert.deepStrictEqual( + _logRecordLimits, + loadDefaultConfig().logRecordLimits + ); + }); + + it('should have default forceFlushTimeoutMillis if not pass', () => { + const provider = new LoggerProvider(); + // @ts-expect-error + const { _activeProcessor } = provider; + assert.ok( + // @ts-expect-error + _activeProcessor._forceFlushTimeoutMillis === + loadDefaultConfig().forceFlushTimeoutMillis + ); + }); + }); + + describe('getLogger', () => { + const testName = 'test name'; + const testVersion = 'test version'; + const testSchemaURL = 'test schema url'; + + it("should create a logger instance if the name doesn't exist", () => { + const provider = new LoggerProvider(); + // @ts-expect-error + assert.ok(provider._loggers.size === 0); + provider.getLogger(testName); + // @ts-expect-error + assert.ok(provider._loggers.size === 1); + }); + + it('should create A new object if the name & version & schemaUrl are not unique', () => { + const provider = new LoggerProvider(); + // @ts-expect-error + assert.ok(provider._loggers.size === 0); + + provider.getLogger(testName); + // @ts-expect-error + assert.ok(provider._loggers.size === 1); + provider.getLogger(testName, testVersion); + // @ts-expect-error + assert.ok(provider._loggers.size === 2); + provider.getLogger(testName, testVersion, { schemaUrl: testSchemaURL }); + // @ts-expect-error + assert.ok(provider._loggers.size === 3); + }); + + it('should not create A new object if the name & version & schemaUrl are unique', () => { + const provider = new LoggerProvider(); + + // @ts-expect-error + assert.ok(provider._loggers.size === 0); + provider.getLogger(testName); + // @ts-expect-error + assert.ok(provider._loggers.size === 1); + const logger1 = provider.getLogger(testName, testVersion, { + schemaUrl: testSchemaURL, + }); + // @ts-expect-error + assert.ok(provider._loggers.size === 2); + const logger2 = provider.getLogger(testName, testVersion, { + schemaUrl: testSchemaURL, + }); + // @ts-expect-error + assert.ok(provider._loggers.size === 2); + assert.ok(logger2 instanceof Logger); + assert.ok(logger1 === logger2); + }); + }); + + describe('addLogRecordProcessor', () => { + it('should add logRecord processor', () => { + // @ts-expect-error + const logRecordProcessor: LogRecordProcessor = {}; + const provider = new LoggerProvider(); + const callSpy = sinon.spy( + // @ts-expect-error + provider._activeProcessor, + 'addLogRecordProcessor' + ); + provider.addLogRecordProcessor(logRecordProcessor); + assert.strictEqual(callSpy.callCount, 1); + }); + }); + + describe('forceFlush', () => { + it('should force flush', async () => { + const provider = new LoggerProvider(); + const callSpy = sinon.spy( + // @ts-expect-error + provider._activeProcessor, + 'forceFlush' + ); + await provider.forceFlush(); + assert.strictEqual(callSpy.callCount, 1); + }); + }); + + describe('shutdown', () => { + it('should shutdown', async () => { + const provider = new LoggerProvider(); + const callSpy = sinon.spy( + // @ts-expect-error + provider._activeProcessor, + 'shutdown' + ); + await provider.shutdown(); + assert.strictEqual(callSpy.callCount, 1); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts new file mode 100644 index 0000000000..1a7325756d --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts @@ -0,0 +1,128 @@ +/* + * 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 type { ReadableLogRecord } from '../../src'; +import { loadDefaultConfig } from '../../src/config'; +import { MultiLogRecordProcessor } from './../../src/MultiLogRecordProcessor'; + +const setup = () => { + const { forceFlushTimeoutMillis } = loadDefaultConfig(); + const multiLogRecordProcessor = new MultiLogRecordProcessor( + forceFlushTimeoutMillis + ); + return { multiLogRecordProcessor, forceFlushTimeoutMillis }; +}; + +describe('MultiLogRecordProcessor', () => { + describe('constructor', () => { + it('should contruct an instance', () => { + assert.ok( + setup().multiLogRecordProcessor instanceof MultiLogRecordProcessor + ); + }); + }); + + describe('addLogRecordProcessor', () => { + it('should add logRecord processor', () => { + const { multiLogRecordProcessor } = setup(); + // @ts-expect-error + assert.ok(multiLogRecordProcessor._processors.length === 0); + // @ts-expect-error + const logRecordProcessor: LogRecordProcessor = {}; + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + // @ts-expect-error + assert.ok(multiLogRecordProcessor._processors.length === 1); + }); + }); + + describe('forceFlush', () => { + it('should force flush all logRecord processors', async () => { + const { multiLogRecordProcessor } = setup(); + const forceFlushSpy = sinon.spy(); + // @ts-expect-error + const logRecordProcessor: LogRecordProcessor = { + forceFlush: forceFlushSpy, + }; + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + await multiLogRecordProcessor.forceFlush(); + assert.strictEqual(forceFlushSpy.callCount, 2); + }); + + it('should throw error if either time out', async () => { + const clock = sinon.useFakeTimers(); + const { multiLogRecordProcessor, forceFlushTimeoutMillis } = setup(); + // @ts-expect-error + assert.ok(multiLogRecordProcessor._processors.length === 0); + // @ts-expect-error + const logRecordProcessorWithTimeout: LogRecordProcessor = { + forceFlush: () => + new Promise(resolve => { + setTimeout(() => { + resolve(); + }, forceFlushTimeoutMillis + 1000); + }), + }; + // @ts-expect-error + const logRecordProcessor: LogRecordProcessor = { + forceFlush: () => Promise.resolve(), + }; + multiLogRecordProcessor.addLogRecordProcessor( + logRecordProcessorWithTimeout + ); + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + const res = multiLogRecordProcessor.forceFlush(); + clock.tick(forceFlushTimeoutMillis + 1000); + clock.restore(); + await assert.rejects(res, /Operation timed out/); + }); + }); + + describe('onEmit', () => { + it('should onEmit all logRecord processors', async () => { + const { multiLogRecordProcessor } = setup(); + const onEmitSpy = sinon.spy(); + // @ts-expect-error + const logRecordProcessor: LogRecordProcessor = { + onEmit: onEmitSpy, + }; + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + // @ts-expect-error + const logRecord: ReadableLogRecord = {}; + multiLogRecordProcessor.onEmit(logRecord); + assert.strictEqual(onEmitSpy.callCount, 2); + }); + }); + + describe('shutdown', () => { + it('should shutdown all logRecord processors', async () => { + const { multiLogRecordProcessor } = setup(); + const shutdownSpy = sinon.spy(); + // @ts-expect-error + const logRecordProcessor: LogRecordProcessor = { + shutdown: shutdownSpy, + }; + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); + multiLogRecordProcessor.shutdown(); + assert.strictEqual(shutdownSpy.callCount, 2); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts new file mode 100644 index 0000000000..94c50e9c62 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts @@ -0,0 +1,344 @@ +/* + * 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 { Resource } from '@opentelemetry/resources'; +import { + ExportResultCode, + getEnv, + loggingErrorHandler, + setGlobalErrorHandler, +} from '@opentelemetry/core'; + +import type { BufferConfig, LogRecordLimits } from '../../../src'; +import { + BatchLogRecordProcessorBase, + LogRecord, + InMemoryLogRecordExporter, +} from '../../../src'; +import { loadDefaultConfig } from '../../../src/config'; +import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; + +class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { + onInit() {} + onShutdown() {} +} + +const createLogRecord = (limits?: LogRecordLimits): LogRecord => { + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const logRecord = new LogRecord( + { + activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), + resource: Resource.default(), + logRecordLimits: { + attributeValueLengthLimit: + limits?.attributeValueLengthLimit ?? + logRecordLimits.attributeValueLengthLimit, + attributeCountLimit: + limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, + }, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }, + {} + ); + return logRecord; +}; + +describe('BatchLogRecordProcessorBase', () => { + const defaultBufferConfig = { + maxExportBatchSize: 5, + scheduledDelayMillis: 2500, + }; + let exporter: InMemoryLogRecordExporter; + + beforeEach(() => { + exporter = new InMemoryLogRecordExporter(); + }); + + afterEach(() => { + exporter.reset(); + sinon.restore(); + }); + + describe('constructor', () => { + it('should create a BatchLogRecordProcessor instance', () => { + const processor = new BatchLogRecordProcessor(exporter); + assert.ok(processor instanceof BatchLogRecordProcessor); + }); + + it('should create a BatchLogRecordProcessor instance with config', () => { + const bufferConfig = { + maxExportBatchSize: 5, + scheduledDelayMillis: 2500, + exportTimeoutMillis: 2000, + maxQueueSize: 200, + }; + const processor = new BatchLogRecordProcessor(exporter, bufferConfig); + assert.ok(processor instanceof BatchLogRecordProcessor); + assert.ok( + // @ts-expect-error + processor._maxExportBatchSize === bufferConfig.maxExportBatchSize + ); + assert.ok( + // @ts-expect-error + processor._maxQueueSize === bufferConfig.maxQueueSize + ); + assert.ok( + // @ts-expect-error + processor._scheduledDelayMillis === bufferConfig.scheduledDelayMillis + ); + assert.ok( + // @ts-expect-error + processor._exportTimeoutMillis === bufferConfig.exportTimeoutMillis + ); + }); + + it('should create a BatchLogRecordProcessor instance with empty config', () => { + const processor = new BatchLogRecordProcessor(exporter); + + const { + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_SCHEDULE_DELAY, + OTEL_BSP_EXPORT_TIMEOUT, + } = getEnv(); + assert.ok(processor instanceof BatchLogRecordProcessor); + assert.ok( + // @ts-expect-error + processor._maxExportBatchSize === OTEL_BSP_MAX_EXPORT_BATCH_SIZE + ); + assert.ok( + // @ts-expect-error + processor._maxQueueSize === OTEL_BSP_MAX_QUEUE_SIZE + ); + assert.ok( + // @ts-expect-error + processor._scheduledDelayMillis === OTEL_BSP_SCHEDULE_DELAY + ); + assert.ok( + // @ts-expect-error + processor._exportTimeoutMillis === OTEL_BSP_EXPORT_TIMEOUT + ); + }); + + it('maxExportBatchSize must be smaller or equal to maxQueueSize', () => { + const bufferConfig = { + maxExportBatchSize: 200, + maxQueueSize: 100, + }; + const processor = new BatchLogRecordProcessor(exporter, bufferConfig); + // @ts-expect-error + const { _maxExportBatchSize, _maxQueueSize } = processor; + assert.ok(_maxExportBatchSize === _maxQueueSize); + }); + }); + + describe('onEmit', () => { + it('should export the log records with buffer size reached', done => { + const clock = sinon.useFakeTimers(); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + processor.onEmit(logRecord); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + } + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + setTimeout(async () => { + assert.strictEqual( + exporter.getFinishedLogRecords().length, + defaultBufferConfig.maxExportBatchSize + ); + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + done(); + }, defaultBufferConfig.scheduledDelayMillis + 1000); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + clock.restore(); + }); + + it('should force flush when timeout exceeded', done => { + const clock = sinon.useFakeTimers(); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + } + setTimeout(() => { + assert.strictEqual( + exporter.getFinishedLogRecords().length, + defaultBufferConfig.maxExportBatchSize + ); + done(); + }, defaultBufferConfig.scheduledDelayMillis + 1000); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + clock.restore(); + }); + + it('should not export empty log record lists', done => { + const spy = sinon.spy(exporter, 'export'); + const clock = sinon.useFakeTimers(); + new BatchLogRecordProcessor(exporter, defaultBufferConfig); + setTimeout(() => { + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + sinon.assert.notCalled(spy); + done(); + }, defaultBufferConfig.scheduledDelayMillis + 1000); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + + clock.restore(); + }); + + it('should export each log record exactly once with buffer size reached multiple times', done => { + const originalTimeout = setTimeout; + const clock = sinon.useFakeTimers(); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + const totalLogRecords = defaultBufferConfig.maxExportBatchSize * 2; + for (let i = 0; i < totalLogRecords; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + } + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); + originalTimeout(() => { + clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); + originalTimeout(async () => { + clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); + clock.restore(); + assert.strictEqual( + exporter.getFinishedLogRecords().length, + totalLogRecords + 1 + ); + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + done(); + }); + }); + }); + + it('should call globalErrorHandler when exporting fails', done => { + const clock = sinon.useFakeTimers(); + const expectedError = new Error('Exporter failed'); + sinon.stub(exporter, 'export').callsFake((_, callback) => { + setTimeout(() => { + callback({ code: ExportResultCode.FAILED, error: expectedError }); + }, 0); + }); + const errorHandlerSpy = sinon.spy(); + setGlobalErrorHandler(errorHandlerSpy); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + } + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + clock.restore(); + setTimeout(() => { + assert.strictEqual(errorHandlerSpy.callCount, 1); + const [[error]] = errorHandlerSpy.args; + assert.deepStrictEqual(error, expectedError); + // reset global error handler + setGlobalErrorHandler(loggingErrorHandler()); + done(); + }); + }); + + it('should drop logRecords when there are more logRecords then "maxQueueSize"', () => { + const maxQueueSize = 6; + const processor = new BatchLogRecordProcessor(exporter, { maxQueueSize }); + const logRecord = createLogRecord(); + for (let i = 0; i < maxQueueSize + 10; i++) { + processor.onEmit(logRecord); + } + // @ts-expect-error + assert.strictEqual(processor._finishedLogRecords.length, 6); + }); + }); + + describe('forceFlush', () => { + it('should force flush on demand', () => { + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + } + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + processor.forceFlush(); + assert.strictEqual( + exporter.getFinishedLogRecords().length, + defaultBufferConfig.maxExportBatchSize + ); + }); + + it('should call an async callback when flushing is complete', async () => { + const processor = new BatchLogRecordProcessor(exporter); + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + await processor.forceFlush(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 1); + }); + }); + + describe('shutdown', () => { + it('should call onShutdown', async () => { + const processor = new BatchLogRecordProcessor(exporter); + const onShutdownSpy = sinon.stub(processor, 'onShutdown'); + assert.strictEqual(onShutdownSpy.callCount, 0); + await processor.shutdown(); + assert.strictEqual(onShutdownSpy.callCount, 1); + }); + + it('should call an async callback when shutdown is complete', async () => { + let exportedLogRecords = 0; + sinon.stub(exporter, 'export').callsFake((logRecords, callback) => { + setTimeout(() => { + exportedLogRecords = exportedLogRecords + logRecords.length; + callback({ code: ExportResultCode.SUCCESS }); + }, 0); + }); + const processor = new BatchLogRecordProcessor(exporter); + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + assert.strictEqual(exportedLogRecords, 1); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts new file mode 100644 index 0000000000..a9f3a3e2ac --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.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 type { ReadableLogRecord } from './../../../src'; +import { ConsoleLogRecordExporter } from '../../../src'; + +describe('ConsoleLogRecordExporter', () => { + let previousConsoleDir: typeof console.dir; + + beforeEach(() => { + previousConsoleDir = console.dir; + console.dir = () => {}; + }); + + afterEach(() => { + console.dir = previousConsoleDir; + }); + + describe('export', () => { + it('should export information about log record', done => { + const consoleExporter = new ConsoleLogRecordExporter(); + const spyConsole = sinon.spy(console, 'dir'); + const logs: ReadableLogRecord[] = []; + for (let i = 0; i < 10; i++) { + // @ts-expect-error + logs.push({ time: [0, 0] }); + } + consoleExporter.export(logs, () => { + assert.strictEqual(spyConsole.callCount, logs.length); + done(); + }); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts new file mode 100644 index 0000000000..68fd4af801 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts @@ -0,0 +1,91 @@ +/* + * 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 type { ReadableLogRecord } from "../../../src"; +import { InMemoryLogRecordExporter } from "../../../src"; + +describe("InMemoryLogRecordExporter", () => { + describe("export", () => { + it("should export information about log record", (done) => { + const memoryExporter = new InMemoryLogRecordExporter(); + const logs: ReadableLogRecord[] = []; + for (let i = 0; i < 10; i++) { + // @ts-expect-error + logs.push({}); + } + memoryExporter.export(logs, () => { + // @ts-expect-error + assert.strictEqual(memoryExporter._finishedLogRecords.length, logs.length); + done(); + }); + }); + }); + + describe("shutdown", () => { + it("should clear all log records", (done) => { + const memoryExporter = new InMemoryLogRecordExporter(); + const logs: ReadableLogRecord[] = []; + for (let i = 0; i < 10; i++) { + // @ts-expect-error + logs.push({}); + } + memoryExporter.export(logs, () => { + // @ts-expect-error + assert.strictEqual(memoryExporter._finishedLogRecords.length, logs.length); + memoryExporter.shutdown(); + // @ts-expect-error + assert.strictEqual(memoryExporter._finishedLogRecords.length, 0); + done(); + }); + }); + }); + + describe("getFinishedLogRecords", () => { + it("should get all log records", (done) => { + const memoryExporter = new InMemoryLogRecordExporter(); + const logs: ReadableLogRecord[] = []; + for (let i = 0; i < 10; i++) { + // @ts-expect-error + logs.push({}); + } + memoryExporter.export(logs, () => { + assert.strictEqual(memoryExporter.getFinishedLogRecords().length, logs.length); + done(); + }); + }); + }); + + describe("reset", () => { + it("should clear all log records", (done) => { + const memoryExporter = new InMemoryLogRecordExporter(); + const logs: ReadableLogRecord[] = []; + for (let i = 0; i < 10; i++) { + // @ts-expect-error + logs.push({}); + } + memoryExporter.export(logs, () => { + // @ts-expect-error + assert.strictEqual(memoryExporter._finishedLogRecords.length, logs.length); + memoryExporter.reset(); + // @ts-expect-error + assert.strictEqual(memoryExporter._finishedLogRecords.length, 0); + done(); + }); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts new file mode 100644 index 0000000000..d0d365c1e0 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from "assert"; +import * as sinon from "sinon"; +import { ExportResultCode, loggingErrorHandler, setGlobalErrorHandler } from "@opentelemetry/core"; + +import type { LogRecordExporter, ReadableLogRecord } from "./../../../src"; +import { SimpleLogRecordProcessor } from "./../../../src"; + +const setup = (exporter?: LogRecordExporter) => { + // @ts-expect-error + const logRecordExporter: LogRecordExporter = exporter || {}; + const processor = new SimpleLogRecordProcessor(logRecordExporter); + return { processor }; +}; + +describe("SimpleLogRecordProcessor", () => { + describe("constructor", () => { + it("should create a SimpleLogRecordProcessor instance", () => { + assert.ok(setup().processor instanceof SimpleLogRecordProcessor); + }); + }); + + describe("onEmit", () => { + it("should handle onEmit", async () => { + const exportSpy = sinon.spy(); + // @ts-expect-error + const { processor } = setup({ export: exportSpy }); + // @ts-expect-error + const logRecord: ReadableLogRecord = {}; + processor.onEmit(logRecord); + assert.ok(exportSpy.callCount === 1); + }); + + it("should call globalErrorHandler when exporting fails", async () => { + const expectedError = new Error("Exporter failed"); + // @ts-expect-error + const exporter: LogRecordExporter = { + export: (_, callback) => setTimeout(() => callback({ code: ExportResultCode.FAILED, error: expectedError }), 0), + }; + const { processor } = setup(exporter); + // @ts-expect-error + const logRecord: ReadableLogRecord = {}; + const errorHandlerSpy = sinon.spy(); + setGlobalErrorHandler(errorHandlerSpy); + processor.onEmit(logRecord); + await new Promise((resolve) => setTimeout(() => resolve(), 0)); + assert.strictEqual(errorHandlerSpy.callCount, 1); + const [[error]] = errorHandlerSpy.args; + assert.deepStrictEqual(error, expectedError); + // reset global error handler + setGlobalErrorHandler(loggingErrorHandler()); + }); + }); + + describe("shutdown", () => { + it("should handle shutdown", async () => { + const shutdownSpy = sinon.spy(); + // @ts-expect-error + const { processor } = setup({ shutdown: shutdownSpy }); + processor.shutdown(); + assert.ok(shutdownSpy.callCount === 1); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/utils.ts b/experimental/packages/sdk-logs/test/common/utils.ts new file mode 100644 index 0000000000..6a30b4e8a6 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/utils.ts @@ -0,0 +1,33 @@ +/* + * 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 const validAttributes = { + string: "string", + number: 0, + bool: true, + "array": ["str1", "str2"], + "array": [1, 2], + "array": [true, false], +}; + +export const invalidAttributes = { + // invalid attribute type object + object: { foo: "bar" }, + // invalid attribute inhomogeneous array + "non-homogeneous-array": [0, ""], + // This empty length attribute should not be set + "": "empty-key", +}; From 5fb3f641b0f01128bceaeaf3d1addb1543ed4593 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 18 Jan 2023 22:24:14 +0800 Subject: [PATCH 05/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/README.md | 35 ++++++++++-------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/experimental/packages/sdk-logs/README.md b/experimental/packages/sdk-logs/README.md index 76bb6d4eac..1ef5ca4692 100644 --- a/experimental/packages/sdk-logs/README.md +++ b/experimental/packages/sdk-logs/README.md @@ -23,12 +23,12 @@ npm install --save @opentelemetry/sdk-logs The basic setup of the SDK can be seen as followings: ```js -const logsAPI = require("@opentelemetry/api-logs"); +const logsAPI = require('@opentelemetry/api-logs'); const { LoggerProvider, SimpleLogRecordProcessor, ConsoleLogRecordExporter, -} = require("@opentelemetry/sdk-logs"); +} = require('@opentelemetry/sdk-logs'); // To start a logger, you first need to initialize the Logger provider. const loggerProvider = new LoggerProvider(); @@ -38,31 +38,26 @@ loggerProvider.addLogRecordProcessor( ); // To create a log record, you first need to get a Logger instance -const logger = loggerProvider.getLogger("default"); +const logger = loggerProvider.getLogger('default'); // You can also use global singleton logsAPI.logs.setGlobalLoggerProvider(loggerProvider); -const logger = logsAPI.logs.getLogger("default"); +const logger = logsAPI.logs.getLogger('default'); // logging an event in an instrumentation library -logger - .getLogEvent("event-name", { - severityNumber: SeverityNumber.WARN, - severityText: "WARN", - body: "this is a log event body", - attributes: { "log.type": "LogEvent" }, - }) - .emit(); +logger.emitEvent({ + name: 'event-name', + body: 'this is a log event body', + attributes: { 'log.type': 'LogEvent' }, +}); // logging an event in a log appender -logger - .getLogRecord({ - severityNumber: SeverityNumber.INFO, - severityText: "INFO", - body: "this is a log record body", - attributes: { "log.type": "LogRecord" }, - }) - .emit(); +logger.emitLogRecord({ + severityNumber: SeverityNumber.INFO, + severityText: 'INFO', + body: 'this is a log record body', + attributes: { 'log.type': 'LogRecord' }, +}); ``` ## Config From 76e66c144f916c252ad361cba27571b00be00718 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 18 Jan 2023 22:41:16 +0800 Subject: [PATCH 06/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/.eslintrc.js | 2 +- experimental/packages/sdk-logs/karma.conf.js | 6 +- .../packages/sdk-logs/src/Attributes.ts | 4 +- .../sdk-logs/src/LogRecordProcessor.ts | 2 +- experimental/packages/sdk-logs/src/config.ts | 7 ++- experimental/packages/sdk-logs/src/index.ts | 24 ++++---- .../sdk-logs/src/platform/browser/index.ts | 2 +- .../packages/sdk-logs/src/platform/index.ts | 2 +- .../node/export/BatchLogRecordProcessor.ts | 4 +- .../sdk-logs/src/platform/node/index.ts | 2 +- .../export/BatchLogRecordProcessor.test.ts | 59 ++++++++++--------- .../test/common/LoggerProvider.test.ts | 1 - .../export/InMemoryLogRecordExporter.test.ts | 44 +++++++++----- .../export/SimpleLogRecordProcessor.test.ts | 41 ++++++++----- .../packages/sdk-logs/test/common/utils.ts | 14 ++--- lerna.json | 1 - 16 files changed, 118 insertions(+), 97 deletions(-) diff --git a/experimental/packages/sdk-logs/.eslintrc.js b/experimental/packages/sdk-logs/.eslintrc.js index 029ade4634..0c986a856c 100644 --- a/experimental/packages/sdk-logs/.eslintrc.js +++ b/experimental/packages/sdk-logs/.eslintrc.js @@ -3,5 +3,5 @@ module.exports = { mocha: true, node: true, }, - ...require("../../../eslint.config.js"), + ...require('../../../eslint.config.js'), }; diff --git a/experimental/packages/sdk-logs/karma.conf.js b/experimental/packages/sdk-logs/karma.conf.js index 4a4a8ff59f..d1c8fbaac1 100644 --- a/experimental/packages/sdk-logs/karma.conf.js +++ b/experimental/packages/sdk-logs/karma.conf.js @@ -14,10 +14,10 @@ * limitations under the License. */ -const karmaWebpackConfig = require("../../../karma.webpack"); -const karmaBaseConfig = require("../../../karma.base"); +const karmaWebpackConfig = require('../../../karma.webpack'); +const karmaBaseConfig = require('../../../karma.base'); -module.exports = (config) => { +module.exports = config => { config.set( Object.assign({}, karmaBaseConfig, { webpack: karmaWebpackConfig, diff --git a/experimental/packages/sdk-logs/src/Attributes.ts b/experimental/packages/sdk-logs/src/Attributes.ts index 56cf1ee7cc..0815dc2c1f 100644 --- a/experimental/packages/sdk-logs/src/Attributes.ts +++ b/experimental/packages/sdk-logs/src/Attributes.ts @@ -15,6 +15,6 @@ */ export const EVENT_LOGS_ATTRIBUTES = { - name: "event.name", - domain: "event.domain", + name: 'event.name', + domain: 'event.domain', }; diff --git a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts index 8dae4eb691..6b81e27a63 100644 --- a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReadableLogRecord } from "./export/ReadableLogRecord"; +import type { ReadableLogRecord } from './export/ReadableLogRecord'; export interface LogRecordProcessor { /** diff --git a/experimental/packages/sdk-logs/src/config.ts b/experimental/packages/sdk-logs/src/config.ts index 0e8e11c1db..40f60e25fd 100644 --- a/experimental/packages/sdk-logs/src/config.ts +++ b/experimental/packages/sdk-logs/src/config.ts @@ -14,16 +14,17 @@ * limitations under the License. */ -import { getEnv } from "@opentelemetry/core"; +import { getEnv } from '@opentelemetry/core'; export function loadDefaultConfig() { return { forceFlushTimeoutMillis: 30000, logRecordLimits: { - attributeValueLengthLimit: getEnv().OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, + attributeValueLengthLimit: + getEnv().OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, attributeCountLimit: getEnv().OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT, }, }; } -export const DEFAULT_EVENT_DOMAIN = "default"; +export const DEFAULT_EVENT_DOMAIN = 'default'; diff --git a/experimental/packages/sdk-logs/src/index.ts b/experimental/packages/sdk-logs/src/index.ts index 62acee26ba..1d5de184dd 100644 --- a/experimental/packages/sdk-logs/src/index.ts +++ b/experimental/packages/sdk-logs/src/index.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -export * from "./types"; -export * from "./LoggerProvider"; -export * from "./Logger"; -export * from "./LogRecord"; -export * from "./LogRecordProcessor"; -export * from "./export/ReadableLogRecord"; -export * from "./export/BatchLogRecordProcessor"; -export * from "./export/ConsoleLogRecordExporter"; -export * from "./export/LogRecordExporter"; -export * from "./export/SimpleLogRecordProcessor"; -export * from "./export/InMemoryLogRecordExporter"; -export * from "./platform"; +export * from './types'; +export * from './LoggerProvider'; +export * from './Logger'; +export * from './LogRecord'; +export * from './LogRecordProcessor'; +export * from './export/ReadableLogRecord'; +export * from './export/BatchLogRecordProcessor'; +export * from './export/ConsoleLogRecordExporter'; +export * from './export/LogRecordExporter'; +export * from './export/SimpleLogRecordProcessor'; +export * from './export/InMemoryLogRecordExporter'; +export * from './platform'; diff --git a/experimental/packages/sdk-logs/src/platform/browser/index.ts b/experimental/packages/sdk-logs/src/platform/browser/index.ts index 5955ac152a..6467530229 100644 --- a/experimental/packages/sdk-logs/src/platform/browser/index.ts +++ b/experimental/packages/sdk-logs/src/platform/browser/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from "./export/BatchLogRecordProcessor"; +export * from './export/BatchLogRecordProcessor'; diff --git a/experimental/packages/sdk-logs/src/platform/index.ts b/experimental/packages/sdk-logs/src/platform/index.ts index 3949243f75..cdaf8858ce 100644 --- a/experimental/packages/sdk-logs/src/platform/index.ts +++ b/experimental/packages/sdk-logs/src/platform/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from "./node"; +export * from './node'; diff --git a/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts index 0319087a30..70bb073f5c 100644 --- a/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type { BufferConfig } from "../../../types"; -import { BatchLogRecordProcessorBase } from "../../../export/BatchLogRecordProcessor"; +import type { BufferConfig } from '../../../types'; +import { BatchLogRecordProcessorBase } from '../../../export/BatchLogRecordProcessor'; export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { protected onShutdown(): void {} diff --git a/experimental/packages/sdk-logs/src/platform/node/index.ts b/experimental/packages/sdk-logs/src/platform/node/index.ts index 5955ac152a..6467530229 100644 --- a/experimental/packages/sdk-logs/src/platform/node/index.ts +++ b/experimental/packages/sdk-logs/src/platform/node/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from "./export/BatchLogRecordProcessor"; +export * from './export/BatchLogRecordProcessor'; diff --git a/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts index 05ffc546c9..e176f3e7dd 100644 --- a/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts @@ -14,17 +14,18 @@ * limitations under the License. */ -import * as assert from "assert"; -import * as sinon from "sinon"; +import * as assert from 'assert'; +import * as sinon from 'sinon'; -import { LogRecordExporter } from "../../../src"; -import { BatchLogRecordProcessor } from "../../../src/platform/browser/export/BatchLogRecordProcessor"; -import { InMemoryLogRecordExporter } from "./../../../src/export/InMemoryLogRecordExporter"; +import { LogRecordExporter } from '../../../src'; +import { BatchLogRecordProcessor } from '../../../src/platform/browser/export/BatchLogRecordProcessor'; +import { InMemoryLogRecordExporter } from './../../../src/export/InMemoryLogRecordExporter'; -const describeDocument = typeof document === "object" ? describe : describe.skip; +const describeDocument = + typeof document === 'object' ? describe : describe.skip; -describeDocument("BatchLogRecordProcessor - web main context", () => { - let visibilityState: VisibilityState = "visible"; +describeDocument('BatchLogRecordProcessor - web main context', () => { + let visibilityState: VisibilityState = 'visible'; let exporter: LogRecordExporter; let processor: BatchLogRecordProcessor; let forceFlushSpy: sinon.SinonStub; @@ -32,29 +33,29 @@ describeDocument("BatchLogRecordProcessor - web main context", () => { let pageHideEvent: Event; beforeEach(() => { - sinon.replaceGetter(document, "visibilityState", () => visibilityState); - visibilityState = "visible"; + sinon.replaceGetter(document, 'visibilityState', () => visibilityState); + visibilityState = 'visible'; exporter = new InMemoryLogRecordExporter(); processor = new BatchLogRecordProcessor(exporter, {}); - forceFlushSpy = sinon.stub(processor, "forceFlush"); - visibilityChangeEvent = new Event("visibilitychange"); - pageHideEvent = new Event("pagehide"); + forceFlushSpy = sinon.stub(processor, 'forceFlush'); + visibilityChangeEvent = new Event('visibilitychange'); + pageHideEvent = new Event('pagehide'); }); afterEach(async () => { sinon.restore(); }); - describe("when document becomes hidden", () => { + describe('when document becomes hidden', () => { const testDocumentHide = (hideDocument: () => void) => { - it("should force flush log records", () => { + it('should force flush log records', () => { assert.strictEqual(forceFlushSpy.callCount, 0); hideDocument(); assert.strictEqual(forceFlushSpy.callCount, 1); }); - describe("AND shutdown has been called", () => { - it("should NOT force flush log records", async () => { + describe('AND shutdown has been called', () => { + it('should NOT force flush log records', async () => { assert.strictEqual(forceFlushSpy.callCount, 0); await processor.shutdown(); hideDocument(); @@ -62,22 +63,22 @@ describeDocument("BatchLogRecordProcessor - web main context", () => { }); }); - describe("AND disableAutoFlushOnDocumentHide configuration option", () => { - it("set to false should force flush log records", () => { + describe('AND disableAutoFlushOnDocumentHide configuration option', () => { + it('set to false should force flush log records', () => { processor = new BatchLogRecordProcessor(exporter, { disableAutoFlushOnDocumentHide: false, }); - forceFlushSpy = sinon.stub(processor, "forceFlush"); + forceFlushSpy = sinon.stub(processor, 'forceFlush'); assert.strictEqual(forceFlushSpy.callCount, 0); hideDocument(); assert.strictEqual(forceFlushSpy.callCount, 1); }); - it("set to true should NOT force flush log records", () => { + it('set to true should NOT force flush log records', () => { processor = new BatchLogRecordProcessor(exporter, { disableAutoFlushOnDocumentHide: true, }); - forceFlushSpy = sinon.stub(processor, "forceFlush"); + forceFlushSpy = sinon.stub(processor, 'forceFlush'); assert.strictEqual(forceFlushSpy.callCount, 0); hideDocument(); assert.strictEqual(forceFlushSpy.callCount, 0); @@ -85,22 +86,22 @@ describeDocument("BatchLogRecordProcessor - web main context", () => { }); }; - describe("by the visibilitychange event", () => { + describe('by the visibilitychange event', () => { testDocumentHide(() => { - visibilityState = "hidden"; + visibilityState = 'hidden'; document.dispatchEvent(visibilityChangeEvent); }); }); - describe("by the pagehide event", () => { + describe('by the pagehide event', () => { testDocumentHide(() => { document.dispatchEvent(pageHideEvent); }); }); }); - describe("when document becomes visible", () => { - it("should NOT force flush log records", () => { + describe('when document becomes visible', () => { + it('should NOT force flush log records', () => { assert.strictEqual(forceFlushSpy.callCount, 0); document.dispatchEvent(visibilityChangeEvent); assert.strictEqual(forceFlushSpy.callCount, 0); @@ -108,8 +109,8 @@ describeDocument("BatchLogRecordProcessor - web main context", () => { }); }); -describe("BatchLogRecordProcessor", () => { - it("without exception", async () => { +describe('BatchLogRecordProcessor', () => { + it('without exception', async () => { const exporter = new InMemoryLogRecordExporter(); const logRecordProcessor = new BatchLogRecordProcessor(exporter); assert.ok(logRecordProcessor instanceof BatchLogRecordProcessor); diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts index e548d75db1..b161bf09bb 100644 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -21,7 +21,6 @@ import { Resource } from '@opentelemetry/resources'; import type { LogRecordProcessor } from './../../src/LogRecordProcessor'; import { Logger, LoggerProvider } from '../../src'; import { loadDefaultConfig } from '../../src/config'; -import { DEFAULT_EVENT_DOMAIN } from './../../src/config'; describe('LoggerProvider', () => { afterEach(() => { diff --git a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts index 68fd4af801..a8c6f0262a 100644 --- a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import * as assert from "assert"; +import * as assert from 'assert'; -import type { ReadableLogRecord } from "../../../src"; -import { InMemoryLogRecordExporter } from "../../../src"; +import type { ReadableLogRecord } from '../../../src'; +import { InMemoryLogRecordExporter } from '../../../src'; -describe("InMemoryLogRecordExporter", () => { - describe("export", () => { - it("should export information about log record", (done) => { +describe('InMemoryLogRecordExporter', () => { + describe('export', () => { + it('should export information about log record', done => { const memoryExporter = new InMemoryLogRecordExporter(); const logs: ReadableLogRecord[] = []; for (let i = 0; i < 10; i++) { @@ -30,14 +30,17 @@ describe("InMemoryLogRecordExporter", () => { } memoryExporter.export(logs, () => { // @ts-expect-error - assert.strictEqual(memoryExporter._finishedLogRecords.length, logs.length); + assert.strictEqual( + memoryExporter._finishedLogRecords.length, + logs.length + ); done(); }); }); }); - describe("shutdown", () => { - it("should clear all log records", (done) => { + describe('shutdown', () => { + it('should clear all log records', done => { const memoryExporter = new InMemoryLogRecordExporter(); const logs: ReadableLogRecord[] = []; for (let i = 0; i < 10; i++) { @@ -46,7 +49,10 @@ describe("InMemoryLogRecordExporter", () => { } memoryExporter.export(logs, () => { // @ts-expect-error - assert.strictEqual(memoryExporter._finishedLogRecords.length, logs.length); + assert.strictEqual( + memoryExporter._finishedLogRecords.length, + logs.length + ); memoryExporter.shutdown(); // @ts-expect-error assert.strictEqual(memoryExporter._finishedLogRecords.length, 0); @@ -55,8 +61,8 @@ describe("InMemoryLogRecordExporter", () => { }); }); - describe("getFinishedLogRecords", () => { - it("should get all log records", (done) => { + describe('getFinishedLogRecords', () => { + it('should get all log records', done => { const memoryExporter = new InMemoryLogRecordExporter(); const logs: ReadableLogRecord[] = []; for (let i = 0; i < 10; i++) { @@ -64,14 +70,17 @@ describe("InMemoryLogRecordExporter", () => { logs.push({}); } memoryExporter.export(logs, () => { - assert.strictEqual(memoryExporter.getFinishedLogRecords().length, logs.length); + assert.strictEqual( + memoryExporter.getFinishedLogRecords().length, + logs.length + ); done(); }); }); }); - describe("reset", () => { - it("should clear all log records", (done) => { + describe('reset', () => { + it('should clear all log records', done => { const memoryExporter = new InMemoryLogRecordExporter(); const logs: ReadableLogRecord[] = []; for (let i = 0; i < 10; i++) { @@ -80,7 +89,10 @@ describe("InMemoryLogRecordExporter", () => { } memoryExporter.export(logs, () => { // @ts-expect-error - assert.strictEqual(memoryExporter._finishedLogRecords.length, logs.length); + assert.strictEqual( + memoryExporter._finishedLogRecords.length, + logs.length + ); memoryExporter.reset(); // @ts-expect-error assert.strictEqual(memoryExporter._finishedLogRecords.length, 0); diff --git a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts index d0d365c1e0..2c4c3fd1d6 100644 --- a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts @@ -14,12 +14,16 @@ * limitations under the License. */ -import * as assert from "assert"; -import * as sinon from "sinon"; -import { ExportResultCode, loggingErrorHandler, setGlobalErrorHandler } from "@opentelemetry/core"; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { + ExportResultCode, + loggingErrorHandler, + setGlobalErrorHandler, +} from '@opentelemetry/core'; -import type { LogRecordExporter, ReadableLogRecord } from "./../../../src"; -import { SimpleLogRecordProcessor } from "./../../../src"; +import type { LogRecordExporter, ReadableLogRecord } from './../../../src'; +import { SimpleLogRecordProcessor } from './../../../src'; const setup = (exporter?: LogRecordExporter) => { // @ts-expect-error @@ -28,15 +32,15 @@ const setup = (exporter?: LogRecordExporter) => { return { processor }; }; -describe("SimpleLogRecordProcessor", () => { - describe("constructor", () => { - it("should create a SimpleLogRecordProcessor instance", () => { +describe('SimpleLogRecordProcessor', () => { + describe('constructor', () => { + it('should create a SimpleLogRecordProcessor instance', () => { assert.ok(setup().processor instanceof SimpleLogRecordProcessor); }); }); - describe("onEmit", () => { - it("should handle onEmit", async () => { + describe('onEmit', () => { + it('should handle onEmit', async () => { const exportSpy = sinon.spy(); // @ts-expect-error const { processor } = setup({ export: exportSpy }); @@ -46,11 +50,16 @@ describe("SimpleLogRecordProcessor", () => { assert.ok(exportSpy.callCount === 1); }); - it("should call globalErrorHandler when exporting fails", async () => { - const expectedError = new Error("Exporter failed"); + it('should call globalErrorHandler when exporting fails', async () => { + const expectedError = new Error('Exporter failed'); // @ts-expect-error const exporter: LogRecordExporter = { - export: (_, callback) => setTimeout(() => callback({ code: ExportResultCode.FAILED, error: expectedError }), 0), + export: (_, callback) => + setTimeout( + () => + callback({ code: ExportResultCode.FAILED, error: expectedError }), + 0 + ), }; const { processor } = setup(exporter); // @ts-expect-error @@ -58,7 +67,7 @@ describe("SimpleLogRecordProcessor", () => { const errorHandlerSpy = sinon.spy(); setGlobalErrorHandler(errorHandlerSpy); processor.onEmit(logRecord); - await new Promise((resolve) => setTimeout(() => resolve(), 0)); + await new Promise(resolve => setTimeout(() => resolve(), 0)); assert.strictEqual(errorHandlerSpy.callCount, 1); const [[error]] = errorHandlerSpy.args; assert.deepStrictEqual(error, expectedError); @@ -67,8 +76,8 @@ describe("SimpleLogRecordProcessor", () => { }); }); - describe("shutdown", () => { - it("should handle shutdown", async () => { + describe('shutdown', () => { + it('should handle shutdown', async () => { const shutdownSpy = sinon.spy(); // @ts-expect-error const { processor } = setup({ shutdown: shutdownSpy }); diff --git a/experimental/packages/sdk-logs/test/common/utils.ts b/experimental/packages/sdk-logs/test/common/utils.ts index 6a30b4e8a6..fd801fc30d 100644 --- a/experimental/packages/sdk-logs/test/common/utils.ts +++ b/experimental/packages/sdk-logs/test/common/utils.ts @@ -15,19 +15,19 @@ */ export const validAttributes = { - string: "string", + string: 'string', number: 0, bool: true, - "array": ["str1", "str2"], - "array": [1, 2], - "array": [true, false], + 'array': ['str1', 'str2'], + 'array': [1, 2], + 'array': [true, false], }; export const invalidAttributes = { // invalid attribute type object - object: { foo: "bar" }, + object: { foo: 'bar' }, // invalid attribute inhomogeneous array - "non-homogeneous-array": [0, ""], + 'non-homogeneous-array': [0, ''], // This empty length attribute should not be set - "": "empty-key", + '': 'empty-key', }; diff --git a/lerna.json b/lerna.json index 57ea824aec..d97ab02a62 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,6 @@ { "version": "independent", "npmClient": "npm", - "useNx": false, "packages": [ "api", "packages/*", From 2214a51fe9d2493e7372974aaff9c3473b245865 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 18 Jan 2023 23:08:11 +0800 Subject: [PATCH 07/44] feat(sdk-logs): sdk-logs init --- packages/opentelemetry-core/src/utils/environment.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index 0b1a3c5f0f..eecd13f486 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -172,7 +172,8 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, - OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT , + OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: + DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT: 128, OTEL_SPAN_LINK_COUNT_LIMIT: 128, From b5a51b4bae1f425bb056ebd3be8ccd0664acbcfd Mon Sep 17 00:00:00 2001 From: Martin Kuba Date: Fri, 17 Feb 2023 17:33:03 -0800 Subject: [PATCH 08/44] fix compile errors --- .../test/common/export/InMemoryLogRecordExporter.test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts index a8c6f0262a..1b8cdff27a 100644 --- a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts @@ -29,9 +29,8 @@ describe('InMemoryLogRecordExporter', () => { logs.push({}); } memoryExporter.export(logs, () => { - // @ts-expect-error assert.strictEqual( - memoryExporter._finishedLogRecords.length, + memoryExporter.getFinishedLogRecords().length, logs.length ); done(); @@ -48,9 +47,8 @@ describe('InMemoryLogRecordExporter', () => { logs.push({}); } memoryExporter.export(logs, () => { - // @ts-expect-error assert.strictEqual( - memoryExporter._finishedLogRecords.length, + memoryExporter.getFinishedLogRecords().length, logs.length ); memoryExporter.shutdown(); @@ -88,9 +86,8 @@ describe('InMemoryLogRecordExporter', () => { logs.push({}); } memoryExporter.export(logs, () => { - // @ts-expect-error assert.strictEqual( - memoryExporter._finishedLogRecords.length, + memoryExporter.getFinishedLogRecords().length, logs.length ); memoryExporter.reset(); From 8f8d25e3f15a2a3411641cca6329c6b765471f89 Mon Sep 17 00:00:00 2001 From: fugao Date: Sat, 18 Feb 2023 14:24:25 +0800 Subject: [PATCH 09/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/package.json | 8 ++++---- .../packages/sdk-logs/src/Attributes.ts | 20 ------------------- experimental/packages/sdk-logs/src/Logger.ts | 14 +------------ .../packages/sdk-logs/src/LoggerProvider.ts | 3 +-- experimental/packages/sdk-logs/src/types.ts | 1 - lerna.json | 1 + 6 files changed, 7 insertions(+), 40 deletions(-) delete mode 100644 experimental/packages/sdk-logs/src/Attributes.ts diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 2dce2849dd..3eccb0da14 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -68,7 +68,7 @@ ], "sideEffects": false, "devDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.4.0", + "@opentelemetry/api": "^1.4.0", "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", @@ -90,8 +90,8 @@ "@opentelemetry/api": ">=1.3.0 <1.4.0" }, "dependencies": { - "@opentelemetry/core": "1.9.0", - "@opentelemetry/resources": "1.9.0", - "@opentelemetry/api-logs": "0.35.0" + "@opentelemetry/core": "1.9.1", + "@opentelemetry/resources": "1.9.1", + "@opentelemetry/api-logs": "0.35.1" } } diff --git a/experimental/packages/sdk-logs/src/Attributes.ts b/experimental/packages/sdk-logs/src/Attributes.ts deleted file mode 100644 index 0815dc2c1f..0000000000 --- a/experimental/packages/sdk-logs/src/Attributes.ts +++ /dev/null @@ -1,20 +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. - */ - -export const EVENT_LOGS_ATTRIBUTES = { - name: 'event.name', - domain: 'event.domain', -}; diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index bdd0a9fa34..93bbcb91f9 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -18,23 +18,11 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import type { LoggerConfig } from './types'; import { LogRecord } from './LogRecord'; -import { EVENT_LOGS_ATTRIBUTES } from './Attributes'; -import { DEFAULT_EVENT_DOMAIN } from './config'; export class Logger implements logsAPI.Logger { constructor(private readonly config: LoggerConfig) {} - public emitEvent(event: logsAPI.LogEvent): void { - const logRecord = new LogRecord(this.config, event); - logRecord.setAttributes({ - [EVENT_LOGS_ATTRIBUTES.name]: event.name, - [EVENT_LOGS_ATTRIBUTES.domain]: - event.domain ?? this.config.eventDomain ?? DEFAULT_EVENT_DOMAIN, - }); - logRecord.emit(); - } - - public emitLogRecord(logRecord: logsAPI.LogRecord): void { + public emit(logRecord: logsAPI.LogRecord): void { new LogRecord(this.config, logRecord).emit(); } } diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index a1314fdbe8..76a54b66b3 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -51,13 +51,12 @@ export class LoggerProvider implements logsAPI.LoggerProvider { version?: string, options?: logsAPI.LoggerOptions ): Logger { - const { schemaUrl = '', eventDomain } = options || {}; + const { schemaUrl = '' } = options || {}; const key = `${name}@${version || ''}:${schemaUrl}`; if (!this._loggers.has(key)) { this._loggers.set( key, new Logger({ - eventDomain, resource: this._resource, logRecordLimits: this._logRecordLimits, activeProcessor: this._activeProcessor, diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index e1c0b144b7..5ac254d1e4 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -41,7 +41,6 @@ export interface LogRecordLimits { } export interface LoggerConfig { - eventDomain?: string; activeProcessor: LogRecordProcessor; resource: Resource; logRecordLimits: LogRecordLimits; diff --git a/lerna.json b/lerna.json index d97ab02a62..5dcb010d3d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,7 @@ { "version": "independent", "npmClient": "npm", + "useNx": "fasle", "packages": [ "api", "packages/*", From 999f97183ab0aa635fa583e28f8a0a7600201ac4 Mon Sep 17 00:00:00 2001 From: fugao Date: Sat, 18 Feb 2023 14:56:50 +0800 Subject: [PATCH 10/44] feat(sdk-logs): sdk-logs init --- .../packages/sdk-logs/src/LogRecord.ts | 4 +- experimental/packages/sdk-logs/src/Logger.ts | 4 +- .../packages/sdk-logs/src/LoggerProvider.ts | 4 +- .../sdk-logs/src/LoggerSharedState.ts | 26 -- .../sdk-logs/src/export/ReadableLogRecord.ts | 4 +- .../browser/export/BatchLogRecordProcessor.ts | 34 +- experimental/packages/sdk-logs/src/types.ts | 6 +- .../export/BatchLogRecordProcessor.test.ts | 121 ------ .../sdk-logs/test/common/LogRecord.test.ts | 243 ------------- .../sdk-logs/test/common/Logger.test.ts | 117 ------ .../test/common/LoggerProvider.test.ts | 158 -------- .../common/MultiLogRecordProcessor.test.ts | 128 ------- .../export/BatchLogRecordProcessor.test.ts | 344 ------------------ .../export/ConsoleLogRecordExporter.test.ts | 50 --- .../export/InMemoryLogRecordExporter.test.ts | 100 ----- .../export/SimpleLogRecordProcessor.test.ts | 88 ----- .../packages/sdk-logs/test/common/utils.ts | 33 -- 17 files changed, 34 insertions(+), 1430 deletions(-) delete mode 100644 experimental/packages/sdk-logs/src/LoggerSharedState.ts delete mode 100644 experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/LogRecord.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/Logger.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts delete mode 100644 experimental/packages/sdk-logs/test/common/utils.ts diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 6f84ece21b..9ca5310894 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -24,7 +24,7 @@ import { isAttributeValue, } from '@opentelemetry/core'; -import type { Resource } from '@opentelemetry/resources'; +import type { IResource } from '@opentelemetry/resources'; import type { ReadableLogRecord } from './export/ReadableLogRecord'; import type { LoggerConfig } from './types'; @@ -36,7 +36,7 @@ export class LogRecord implements ReadableLogRecord { readonly severityText?: string; readonly severityNumber?: logsAPI.SeverityNumber; readonly body?: string; - readonly resource: Resource; + readonly resource: IResource; readonly instrumentationScope: InstrumentationScope; readonly attributes: Attributes = {}; diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index 93bbcb91f9..1111f82584 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -20,9 +20,9 @@ import type { LoggerConfig } from './types'; import { LogRecord } from './LogRecord'; export class Logger implements logsAPI.Logger { - constructor(private readonly config: LoggerConfig) {} + constructor(private readonly _config: LoggerConfig) {} public emit(logRecord: logsAPI.LogRecord): void { - new LogRecord(this.config, logRecord).emit(); + new LogRecord(this._config, logRecord).emit(); } } diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 76a54b66b3..d12055c561 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -15,7 +15,7 @@ */ import type * as logsAPI from '@opentelemetry/api-logs'; -import { Resource } from '@opentelemetry/resources'; +import { IResource, Resource } from '@opentelemetry/resources'; import { merge } from '@opentelemetry/core'; import type { LoggerProviderConfig, LogRecordLimits } from './types'; @@ -26,7 +26,7 @@ import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; export class LoggerProvider implements logsAPI.LoggerProvider { private readonly _loggers: Map = new Map(); - private readonly _resource: Resource; + private readonly _resource: IResource; private readonly _logRecordLimits: LogRecordLimits; private readonly _activeProcessor: MultiLogRecordProcessor; diff --git a/experimental/packages/sdk-logs/src/LoggerSharedState.ts b/experimental/packages/sdk-logs/src/LoggerSharedState.ts deleted file mode 100644 index 6ffd1d1b7c..0000000000 --- a/experimental/packages/sdk-logs/src/LoggerSharedState.ts +++ /dev/null @@ -1,26 +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 type { Resource } from '@opentelemetry/resources'; - -import type { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; -import type { LogRecordLimits } from './types'; - -export interface LoggerSharedState { - readonly activeProcessor: MultiLogRecordProcessor; - readonly resource: Resource; - readonly logRecordLimits: LogRecordLimits; -} diff --git a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts index b211d521c0..99f18b82f9 100644 --- a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts +++ b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Resource } from '@opentelemetry/resources'; +import type { IResource } from '@opentelemetry/resources'; import type { Attributes, HrTime } from '@opentelemetry/api'; import type { InstrumentationScope } from '@opentelemetry/core'; import type { SeverityNumber } from '@opentelemetry/api-logs'; @@ -27,7 +27,7 @@ export interface ReadableLogRecord { readonly severityText?: string; readonly severityNumber?: SeverityNumber; readonly body?: string; - readonly resource: Resource; + readonly resource: IResource; readonly instrumentationScope: InstrumentationScope; readonly attributes: Attributes; } diff --git a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts index ee8d6fc00e..cb268bb94b 100644 --- a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts @@ -14,46 +14,58 @@ * limitations under the License. */ -import type { LogRecordExporter } from "./../../../export/LogRecordExporter"; -import type { BatchLogRecordProcessorBrowserConfig } from "../../../types"; -import { BatchLogRecordProcessorBase } from "../../../export/BatchLogRecordProcessor"; +import type { LogRecordExporter } from './../../../export/LogRecordExporter'; +import type { BatchLogRecordProcessorBrowserConfig } from '../../../types'; +import { BatchLogRecordProcessorBase } from '../../../export/BatchLogRecordProcessor'; export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { private _visibilityChangeListener?: () => void; private _pageHideListener?: () => void; - constructor(exporter: LogRecordExporter, config?: BatchLogRecordProcessorBrowserConfig) { + constructor( + exporter: LogRecordExporter, + config?: BatchLogRecordProcessorBrowserConfig + ) { super(exporter, config); this._onInit(config); } protected onShutdown(): void { - if (typeof document === "undefined") { + if (typeof document === 'undefined') { return; } if (this._visibilityChangeListener) { - document.removeEventListener("visibilitychange", this._visibilityChangeListener); + document.removeEventListener( + 'visibilitychange', + this._visibilityChangeListener + ); } if (this._pageHideListener) { - document.removeEventListener("pagehide", this._pageHideListener); + document.removeEventListener('pagehide', this._pageHideListener); } } private _onInit(config?: BatchLogRecordProcessorBrowserConfig): void { - if (config?.disableAutoFlushOnDocumentHide === true || typeof document === "undefined") { + if ( + config?.disableAutoFlushOnDocumentHide === true || + typeof document === 'undefined' + ) { return; } this._visibilityChangeListener = () => { - if (document.visibilityState === "hidden") { + if (document.visibilityState === 'hidden') { this.forceFlush(); } }; this._pageHideListener = () => { this.forceFlush(); }; - document.addEventListener("visibilitychange", this._visibilityChangeListener); + document.addEventListener( + 'visibilitychange', + this._visibilityChangeListener + ); // use 'pagehide' event as a fallback for Safari; see https://bugs.webkit.org/show_bug.cgi?id=116769 - document.addEventListener("pagehide", this._pageHideListener); + document.addEventListener('pagehide', this._pageHideListener); } } diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index 5ac254d1e4..dcff384d35 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import type { Resource } from '@opentelemetry/resources'; +import type { IResource } from '@opentelemetry/resources'; import type { InstrumentationScope } from '@opentelemetry/core'; import type { LogRecordProcessor } from './LogRecordProcessor'; export interface LoggerProviderConfig { /** Resource associated with trace telemetry */ - resource?: Resource; + resource?: IResource; /** Log Record Limits*/ logRecordLimits?: LogRecordLimits; @@ -42,7 +42,7 @@ export interface LogRecordLimits { export interface LoggerConfig { activeProcessor: LogRecordProcessor; - resource: Resource; + resource: IResource; logRecordLimits: LogRecordLimits; instrumentationScope: InstrumentationScope; } diff --git a/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts deleted file mode 100644 index e176f3e7dd..0000000000 --- a/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts +++ /dev/null @@ -1,121 +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 * as sinon from 'sinon'; - -import { LogRecordExporter } from '../../../src'; -import { BatchLogRecordProcessor } from '../../../src/platform/browser/export/BatchLogRecordProcessor'; -import { InMemoryLogRecordExporter } from './../../../src/export/InMemoryLogRecordExporter'; - -const describeDocument = - typeof document === 'object' ? describe : describe.skip; - -describeDocument('BatchLogRecordProcessor - web main context', () => { - let visibilityState: VisibilityState = 'visible'; - let exporter: LogRecordExporter; - let processor: BatchLogRecordProcessor; - let forceFlushSpy: sinon.SinonStub; - let visibilityChangeEvent: Event; - let pageHideEvent: Event; - - beforeEach(() => { - sinon.replaceGetter(document, 'visibilityState', () => visibilityState); - visibilityState = 'visible'; - exporter = new InMemoryLogRecordExporter(); - processor = new BatchLogRecordProcessor(exporter, {}); - forceFlushSpy = sinon.stub(processor, 'forceFlush'); - visibilityChangeEvent = new Event('visibilitychange'); - pageHideEvent = new Event('pagehide'); - }); - - afterEach(async () => { - sinon.restore(); - }); - - describe('when document becomes hidden', () => { - const testDocumentHide = (hideDocument: () => void) => { - it('should force flush log records', () => { - assert.strictEqual(forceFlushSpy.callCount, 0); - hideDocument(); - assert.strictEqual(forceFlushSpy.callCount, 1); - }); - - describe('AND shutdown has been called', () => { - it('should NOT force flush log records', async () => { - assert.strictEqual(forceFlushSpy.callCount, 0); - await processor.shutdown(); - hideDocument(); - assert.strictEqual(forceFlushSpy.callCount, 0); - }); - }); - - describe('AND disableAutoFlushOnDocumentHide configuration option', () => { - it('set to false should force flush log records', () => { - processor = new BatchLogRecordProcessor(exporter, { - disableAutoFlushOnDocumentHide: false, - }); - forceFlushSpy = sinon.stub(processor, 'forceFlush'); - assert.strictEqual(forceFlushSpy.callCount, 0); - hideDocument(); - assert.strictEqual(forceFlushSpy.callCount, 1); - }); - - it('set to true should NOT force flush log records', () => { - processor = new BatchLogRecordProcessor(exporter, { - disableAutoFlushOnDocumentHide: true, - }); - forceFlushSpy = sinon.stub(processor, 'forceFlush'); - assert.strictEqual(forceFlushSpy.callCount, 0); - hideDocument(); - assert.strictEqual(forceFlushSpy.callCount, 0); - }); - }); - }; - - describe('by the visibilitychange event', () => { - testDocumentHide(() => { - visibilityState = 'hidden'; - document.dispatchEvent(visibilityChangeEvent); - }); - }); - - describe('by the pagehide event', () => { - testDocumentHide(() => { - document.dispatchEvent(pageHideEvent); - }); - }); - }); - - describe('when document becomes visible', () => { - it('should NOT force flush log records', () => { - assert.strictEqual(forceFlushSpy.callCount, 0); - document.dispatchEvent(visibilityChangeEvent); - assert.strictEqual(forceFlushSpy.callCount, 0); - }); - }); -}); - -describe('BatchLogRecordProcessor', () => { - it('without exception', async () => { - const exporter = new InMemoryLogRecordExporter(); - const logRecordProcessor = new BatchLogRecordProcessor(exporter); - assert.ok(logRecordProcessor instanceof BatchLogRecordProcessor); - - await logRecordProcessor.forceFlush(); - await logRecordProcessor.shutdown(); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts deleted file mode 100644 index f8e66a30ff..0000000000 --- a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts +++ /dev/null @@ -1,243 +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 type { Attributes, AttributeValue } from '@opentelemetry/api'; -import { SeverityNumber } from '@opentelemetry/api-logs'; -import * as assert from 'assert'; -import * as sinon from 'sinon'; -import type * as logsAPI from '@opentelemetry/api-logs'; -import { Resource } from '@opentelemetry/resources'; -import { InstrumentationScope, timeInputToHrTime } from '@opentelemetry/core'; - -import type { LogRecordLimits } from './../../src'; -import { LogRecord } from './../../src'; -import { loadDefaultConfig } from '../../src/config'; -import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; -import { invalidAttributes, validAttributes } from './utils'; - -const setup = ( - limits?: LogRecordLimits, - instrumentationScope?: InstrumentationScope, - data?: logsAPI.LogRecord -) => { - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const logRecord = new LogRecord( - { - eventDomain: 'eventDomain', - activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), - resource: Resource.default(), - logRecordLimits: { - attributeValueLengthLimit: - limits?.attributeValueLengthLimit ?? - logRecordLimits.attributeValueLengthLimit, - attributeCountLimit: - limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, - }, - instrumentationScope: instrumentationScope || { name: 'test' }, - }, - data || {} - ); - return { logRecord }; -}; - -describe('LogRecord', () => { - describe('constructor', () => { - it('should create an instance', () => { - const { logRecord } = setup(); - assert.ok(logRecord instanceof LogRecord); - }); - - it('should have a default timestamp', () => { - const { logRecord } = setup(); - const { time } = logRecord; - assert.ok(time !== undefined); - }); - - it('should create an instance with logRecord', () => { - const logRecordData = { - timestamp: new Date().getTime(), - severityNumber: SeverityNumber.DEBUG, - severityText: 'DEBUG', - body: 'this is a body', - attributes: { - name: 'zhangsan', - }, - traceId: 'trance id', - spanId: 'span id', - traceFlags: 1, - }; - const instrumentationScope = { - name: 'name', - }; - - const logRecord = setup( - undefined, - instrumentationScope, - logRecordData - ).logRecord; - assert.deepStrictEqual( - logRecord.time, - timeInputToHrTime(logRecordData.timestamp) - ); - assert.strictEqual( - logRecord.severityNumber, - logRecordData.severityNumber - ); - assert.strictEqual(logRecord.severityText, logRecordData.severityText); - assert.strictEqual(logRecord.body, logRecordData.body); - assert.deepStrictEqual(logRecord.attributes, logRecordData.attributes); - assert.strictEqual(logRecord.traceId, logRecordData.traceId); - assert.strictEqual(logRecord.spanId, logRecordData.spanId); - assert.strictEqual(logRecord.traceFlags, logRecordData.traceFlags); - assert.deepStrictEqual( - logRecord.instrumentationScope, - instrumentationScope - ); - }); - }); - - describe('setAttribute', () => { - describe('when default options set', () => { - it('should set an attribute', () => { - const { logRecord } = setup(); - for (const [k, v] of Object.entries(validAttributes)) { - logRecord.setAttribute(k, v); - } - for (const [k, v] of Object.entries(invalidAttributes)) { - logRecord.setAttribute(k, v as unknown as AttributeValue); - } - assert.deepStrictEqual(logRecord.attributes, validAttributes); - }); - - it('should be able to overwrite attributes', () => { - const { logRecord } = setup(); - logRecord.setAttribute('overwrite', 'initial value'); - logRecord.setAttribute('overwrite', 'overwritten value'); - assert.deepStrictEqual(logRecord.attributes, { - overwrite: 'overwritten value', - }); - }); - }); - - describe('when logRecordLimits options set', () => { - describe('when "attributeCountLimit" option defined', () => { - const { logRecord } = setup({ attributeCountLimit: 100 }); - for (let i = 0; i < 150; i++) { - logRecord.setAttribute(`foo${i}`, `bar${i}`); - } - - it('should remove / drop all remaining values after the number of values exceeds this limit', () => { - const { attributes } = logRecord; - assert.strictEqual(Object.keys(attributes).length, 100); - assert.strictEqual(attributes.foo0, 'bar0'); - assert.strictEqual(attributes.foo99, 'bar99'); - assert.strictEqual(attributes.foo149, undefined); - }); - }); - - describe('when "attributeValueLengthLimit" option defined', () => { - const { logRecord } = setup({ attributeValueLengthLimit: 5 }); - const { attributes } = logRecord; - - it('should truncate value which length exceeds this limit', () => { - logRecord.setAttribute('attr-with-more-length', 'abcdefgh'); - assert.strictEqual(attributes['attr-with-more-length'], 'abcde'); - }); - - it('should truncate value of arrays which exceeds this limit', () => { - logRecord.setAttribute('attr-array-of-strings', [ - 'abcdefgh', - 'abc', - 'abcde', - '', - ]); - logRecord.setAttribute('attr-array-of-bool', [true, false]); - assert.deepStrictEqual(attributes['attr-array-of-strings'], [ - 'abcde', - 'abc', - 'abcde', - '', - ]); - assert.deepStrictEqual(attributes['attr-array-of-bool'], [ - true, - false, - ]); - }); - - it('should not truncate value which length not exceeds this limit', () => { - logRecord.setAttribute('attr-with-less-length', 'abc'); - assert.strictEqual(attributes['attr-with-less-length'], 'abc'); - }); - - it('should return same value for non-string values', () => { - logRecord.setAttribute('attr-non-string', true); - assert.strictEqual(attributes['attr-non-string'], true); - }); - }); - - describe('when "attributeValueLengthLimit" option is invalid', () => { - const { logRecord } = setup({ attributeValueLengthLimit: -5 }); - const { attributes } = logRecord; - - it('should not truncate any value', () => { - logRecord.setAttribute('attr-not-truncate', 'abcdefgh'); - logRecord.setAttribute('attr-array-of-strings', [ - 'abcdefgh', - 'abc', - 'abcde', - ]); - assert.deepStrictEqual(attributes['attr-not-truncate'], 'abcdefgh'); - assert.deepStrictEqual(attributes['attr-array-of-strings'], [ - 'abcdefgh', - 'abc', - 'abcde', - ]); - }); - }); - }); - }); - - describe('setAttributes', () => { - it('should be able to set multiple attributes', () => { - const { logRecord } = setup(); - logRecord.setAttributes(validAttributes); - logRecord.setAttributes(invalidAttributes as unknown as Attributes); - assert.deepStrictEqual(logRecord.attributes, validAttributes); - }); - }); - - describe('emit', () => { - it('should be emit', () => { - const { logRecord } = setup(); - // @ts-expect-error - const callSpy = sinon.spy(logRecord.config.activeProcessor, 'onEmit'); - logRecord.emit(); - assert.ok(callSpy.called); - }); - - it('should be allow emit only once', () => { - const { logRecord } = setup(); - const callSpy = sinon.spy( - // @ts-expect-error - logRecord.config.activeProcessor, - 'onEmit' - ); - logRecord.emit(); - logRecord.emit(); - assert.ok(callSpy.callCount === 1); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts deleted file mode 100644 index eab7d357e1..0000000000 --- a/experimental/packages/sdk-logs/test/common/Logger.test.ts +++ /dev/null @@ -1,117 +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 * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; - -import { Logger, LogRecord } from '../../src'; -import { DEFAULT_EVENT_DOMAIN, loadDefaultConfig } from '../../src/config'; -import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; -import { EVENT_LOGS_ATTRIBUTES } from '../../src/Attributes'; - -const setup = (eventDomain?: string) => { - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const logger = new Logger({ - eventDomain, - // @ts-expect-error - loggerSharedState: { - activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), - resource: Resource.default(), - logRecordLimits, - }, - instrumentationScope: { - name: 'test name', - version: 'test version', - schemaUrl: 'test schema url', - }, - }); - return { logger }; -}; - -describe('Logger', () => { - afterEach(() => { - sinon.restore(); - }); - - describe('constructor', () => { - it('should create an instance', () => { - const { logger } = setup(); - assert.ok(logger instanceof Logger); - }); - }); - - describe('emitLogRecord', () => { - it('should emit log record', () => { - const emitSpy = sinon.stub(LogRecord.prototype, 'emit'); - const { logger } = setup(); - logger.emitLogRecord({}); - assert.strictEqual(emitSpy.callCount, 1); - }); - }); - - describe('emitEvent', () => { - it('should emit log record with event attributes', () => { - const emitSpy = sinon.stub(LogRecord.prototype, 'emit'); - const setAttributesSpy = sinon.stub(LogRecord.prototype, 'setAttributes'); - - const eventName = 'test event name'; - const eventDomain = 'test event domain'; - - const { logger } = setup(eventDomain); - logger.emitEvent({ name: eventName }); - - const eventAttributes = { - [EVENT_LOGS_ATTRIBUTES.name]: eventName, - [EVENT_LOGS_ATTRIBUTES.domain]: eventDomain, - }; - - assert.strictEqual(emitSpy.callCount, 1); - assert.deepEqual(setAttributesSpy.secondCall.args[0], eventAttributes); - }); - - it('should have default eventDomain if not pass', () => { - sinon.stub(LogRecord.prototype, 'emit'); - const setAttributesSpy = sinon.stub(LogRecord.prototype, 'setAttributes'); - - const eventName = 'test event name'; - const { logger } = setup(); - logger.emitEvent({ name: eventName }); - - assert.strictEqual( - setAttributesSpy.secondCall.args[0][EVENT_LOGS_ATTRIBUTES.domain], - DEFAULT_EVENT_DOMAIN - ); - }); - - it('should use eventDomain if LogEvent has eventDomain', () => { - sinon.stub(LogRecord.prototype, 'emit'); - const setAttributesSpy = sinon.stub(LogRecord.prototype, 'setAttributes'); - - const eventName = 'test event name'; - const loggerEventDomain = 'test event domain in logger'; - const emitEventDomain = 'test event domain in emitEvent'; - - const { logger } = setup(loggerEventDomain); - logger.emitEvent({ name: eventName, domain: emitEventDomain }); - - assert.strictEqual( - setAttributesSpy.secondCall.args[0][EVENT_LOGS_ATTRIBUTES.domain], - emitEventDomain - ); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts deleted file mode 100644 index b161bf09bb..0000000000 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ /dev/null @@ -1,158 +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 * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; - -import type { LogRecordProcessor } from './../../src/LogRecordProcessor'; -import { Logger, LoggerProvider } from '../../src'; -import { loadDefaultConfig } from '../../src/config'; - -describe('LoggerProvider', () => { - afterEach(() => { - sinon.restore(); - }); - - describe('constructor', () => { - it('should contruct an instance', () => { - const provider = new LoggerProvider(); - assert.ok(provider instanceof LoggerProvider); - }); - - it('should have default resource if not pass', () => { - const provider = new LoggerProvider(); - // @ts-expect-error - const { _resource } = provider; - assert.deepStrictEqual(_resource, Resource.default()); - }); - - it('should have default logRecordLimits if not pass', () => { - const provider = new LoggerProvider(); - // @ts-expect-error - const { _logRecordLimits } = provider; - assert.deepStrictEqual( - _logRecordLimits, - loadDefaultConfig().logRecordLimits - ); - }); - - it('should have default forceFlushTimeoutMillis if not pass', () => { - const provider = new LoggerProvider(); - // @ts-expect-error - const { _activeProcessor } = provider; - assert.ok( - // @ts-expect-error - _activeProcessor._forceFlushTimeoutMillis === - loadDefaultConfig().forceFlushTimeoutMillis - ); - }); - }); - - describe('getLogger', () => { - const testName = 'test name'; - const testVersion = 'test version'; - const testSchemaURL = 'test schema url'; - - it("should create a logger instance if the name doesn't exist", () => { - const provider = new LoggerProvider(); - // @ts-expect-error - assert.ok(provider._loggers.size === 0); - provider.getLogger(testName); - // @ts-expect-error - assert.ok(provider._loggers.size === 1); - }); - - it('should create A new object if the name & version & schemaUrl are not unique', () => { - const provider = new LoggerProvider(); - // @ts-expect-error - assert.ok(provider._loggers.size === 0); - - provider.getLogger(testName); - // @ts-expect-error - assert.ok(provider._loggers.size === 1); - provider.getLogger(testName, testVersion); - // @ts-expect-error - assert.ok(provider._loggers.size === 2); - provider.getLogger(testName, testVersion, { schemaUrl: testSchemaURL }); - // @ts-expect-error - assert.ok(provider._loggers.size === 3); - }); - - it('should not create A new object if the name & version & schemaUrl are unique', () => { - const provider = new LoggerProvider(); - - // @ts-expect-error - assert.ok(provider._loggers.size === 0); - provider.getLogger(testName); - // @ts-expect-error - assert.ok(provider._loggers.size === 1); - const logger1 = provider.getLogger(testName, testVersion, { - schemaUrl: testSchemaURL, - }); - // @ts-expect-error - assert.ok(provider._loggers.size === 2); - const logger2 = provider.getLogger(testName, testVersion, { - schemaUrl: testSchemaURL, - }); - // @ts-expect-error - assert.ok(provider._loggers.size === 2); - assert.ok(logger2 instanceof Logger); - assert.ok(logger1 === logger2); - }); - }); - - describe('addLogRecordProcessor', () => { - it('should add logRecord processor', () => { - // @ts-expect-error - const logRecordProcessor: LogRecordProcessor = {}; - const provider = new LoggerProvider(); - const callSpy = sinon.spy( - // @ts-expect-error - provider._activeProcessor, - 'addLogRecordProcessor' - ); - provider.addLogRecordProcessor(logRecordProcessor); - assert.strictEqual(callSpy.callCount, 1); - }); - }); - - describe('forceFlush', () => { - it('should force flush', async () => { - const provider = new LoggerProvider(); - const callSpy = sinon.spy( - // @ts-expect-error - provider._activeProcessor, - 'forceFlush' - ); - await provider.forceFlush(); - assert.strictEqual(callSpy.callCount, 1); - }); - }); - - describe('shutdown', () => { - it('should shutdown', async () => { - const provider = new LoggerProvider(); - const callSpy = sinon.spy( - // @ts-expect-error - provider._activeProcessor, - 'shutdown' - ); - await provider.shutdown(); - assert.strictEqual(callSpy.callCount, 1); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts deleted file mode 100644 index 1a7325756d..0000000000 --- a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts +++ /dev/null @@ -1,128 +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 * as sinon from 'sinon'; - -import type { ReadableLogRecord } from '../../src'; -import { loadDefaultConfig } from '../../src/config'; -import { MultiLogRecordProcessor } from './../../src/MultiLogRecordProcessor'; - -const setup = () => { - const { forceFlushTimeoutMillis } = loadDefaultConfig(); - const multiLogRecordProcessor = new MultiLogRecordProcessor( - forceFlushTimeoutMillis - ); - return { multiLogRecordProcessor, forceFlushTimeoutMillis }; -}; - -describe('MultiLogRecordProcessor', () => { - describe('constructor', () => { - it('should contruct an instance', () => { - assert.ok( - setup().multiLogRecordProcessor instanceof MultiLogRecordProcessor - ); - }); - }); - - describe('addLogRecordProcessor', () => { - it('should add logRecord processor', () => { - const { multiLogRecordProcessor } = setup(); - // @ts-expect-error - assert.ok(multiLogRecordProcessor._processors.length === 0); - // @ts-expect-error - const logRecordProcessor: LogRecordProcessor = {}; - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - // @ts-expect-error - assert.ok(multiLogRecordProcessor._processors.length === 1); - }); - }); - - describe('forceFlush', () => { - it('should force flush all logRecord processors', async () => { - const { multiLogRecordProcessor } = setup(); - const forceFlushSpy = sinon.spy(); - // @ts-expect-error - const logRecordProcessor: LogRecordProcessor = { - forceFlush: forceFlushSpy, - }; - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - await multiLogRecordProcessor.forceFlush(); - assert.strictEqual(forceFlushSpy.callCount, 2); - }); - - it('should throw error if either time out', async () => { - const clock = sinon.useFakeTimers(); - const { multiLogRecordProcessor, forceFlushTimeoutMillis } = setup(); - // @ts-expect-error - assert.ok(multiLogRecordProcessor._processors.length === 0); - // @ts-expect-error - const logRecordProcessorWithTimeout: LogRecordProcessor = { - forceFlush: () => - new Promise(resolve => { - setTimeout(() => { - resolve(); - }, forceFlushTimeoutMillis + 1000); - }), - }; - // @ts-expect-error - const logRecordProcessor: LogRecordProcessor = { - forceFlush: () => Promise.resolve(), - }; - multiLogRecordProcessor.addLogRecordProcessor( - logRecordProcessorWithTimeout - ); - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - const res = multiLogRecordProcessor.forceFlush(); - clock.tick(forceFlushTimeoutMillis + 1000); - clock.restore(); - await assert.rejects(res, /Operation timed out/); - }); - }); - - describe('onEmit', () => { - it('should onEmit all logRecord processors', async () => { - const { multiLogRecordProcessor } = setup(); - const onEmitSpy = sinon.spy(); - // @ts-expect-error - const logRecordProcessor: LogRecordProcessor = { - onEmit: onEmitSpy, - }; - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - // @ts-expect-error - const logRecord: ReadableLogRecord = {}; - multiLogRecordProcessor.onEmit(logRecord); - assert.strictEqual(onEmitSpy.callCount, 2); - }); - }); - - describe('shutdown', () => { - it('should shutdown all logRecord processors', async () => { - const { multiLogRecordProcessor } = setup(); - const shutdownSpy = sinon.spy(); - // @ts-expect-error - const logRecordProcessor: LogRecordProcessor = { - shutdown: shutdownSpy, - }; - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - multiLogRecordProcessor.addLogRecordProcessor(logRecordProcessor); - multiLogRecordProcessor.shutdown(); - assert.strictEqual(shutdownSpy.callCount, 2); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts deleted file mode 100644 index 94c50e9c62..0000000000 --- a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts +++ /dev/null @@ -1,344 +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 * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; -import { - ExportResultCode, - getEnv, - loggingErrorHandler, - setGlobalErrorHandler, -} from '@opentelemetry/core'; - -import type { BufferConfig, LogRecordLimits } from '../../../src'; -import { - BatchLogRecordProcessorBase, - LogRecord, - InMemoryLogRecordExporter, -} from '../../../src'; -import { loadDefaultConfig } from '../../../src/config'; -import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; - -class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { - onInit() {} - onShutdown() {} -} - -const createLogRecord = (limits?: LogRecordLimits): LogRecord => { - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const logRecord = new LogRecord( - { - activeProcessor: new MultiLogRecordProcessor(forceFlushTimeoutMillis), - resource: Resource.default(), - logRecordLimits: { - attributeValueLengthLimit: - limits?.attributeValueLengthLimit ?? - logRecordLimits.attributeValueLengthLimit, - attributeCountLimit: - limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, - }, - instrumentationScope: { - name: 'test name', - version: 'test version', - schemaUrl: 'test schema url', - }, - }, - {} - ); - return logRecord; -}; - -describe('BatchLogRecordProcessorBase', () => { - const defaultBufferConfig = { - maxExportBatchSize: 5, - scheduledDelayMillis: 2500, - }; - let exporter: InMemoryLogRecordExporter; - - beforeEach(() => { - exporter = new InMemoryLogRecordExporter(); - }); - - afterEach(() => { - exporter.reset(); - sinon.restore(); - }); - - describe('constructor', () => { - it('should create a BatchLogRecordProcessor instance', () => { - const processor = new BatchLogRecordProcessor(exporter); - assert.ok(processor instanceof BatchLogRecordProcessor); - }); - - it('should create a BatchLogRecordProcessor instance with config', () => { - const bufferConfig = { - maxExportBatchSize: 5, - scheduledDelayMillis: 2500, - exportTimeoutMillis: 2000, - maxQueueSize: 200, - }; - const processor = new BatchLogRecordProcessor(exporter, bufferConfig); - assert.ok(processor instanceof BatchLogRecordProcessor); - assert.ok( - // @ts-expect-error - processor._maxExportBatchSize === bufferConfig.maxExportBatchSize - ); - assert.ok( - // @ts-expect-error - processor._maxQueueSize === bufferConfig.maxQueueSize - ); - assert.ok( - // @ts-expect-error - processor._scheduledDelayMillis === bufferConfig.scheduledDelayMillis - ); - assert.ok( - // @ts-expect-error - processor._exportTimeoutMillis === bufferConfig.exportTimeoutMillis - ); - }); - - it('should create a BatchLogRecordProcessor instance with empty config', () => { - const processor = new BatchLogRecordProcessor(exporter); - - const { - OTEL_BSP_MAX_EXPORT_BATCH_SIZE, - OTEL_BSP_MAX_QUEUE_SIZE, - OTEL_BSP_SCHEDULE_DELAY, - OTEL_BSP_EXPORT_TIMEOUT, - } = getEnv(); - assert.ok(processor instanceof BatchLogRecordProcessor); - assert.ok( - // @ts-expect-error - processor._maxExportBatchSize === OTEL_BSP_MAX_EXPORT_BATCH_SIZE - ); - assert.ok( - // @ts-expect-error - processor._maxQueueSize === OTEL_BSP_MAX_QUEUE_SIZE - ); - assert.ok( - // @ts-expect-error - processor._scheduledDelayMillis === OTEL_BSP_SCHEDULE_DELAY - ); - assert.ok( - // @ts-expect-error - processor._exportTimeoutMillis === OTEL_BSP_EXPORT_TIMEOUT - ); - }); - - it('maxExportBatchSize must be smaller or equal to maxQueueSize', () => { - const bufferConfig = { - maxExportBatchSize: 200, - maxQueueSize: 100, - }; - const processor = new BatchLogRecordProcessor(exporter, bufferConfig); - // @ts-expect-error - const { _maxExportBatchSize, _maxQueueSize } = processor; - assert.ok(_maxExportBatchSize === _maxQueueSize); - }); - }); - - describe('onEmit', () => { - it('should export the log records with buffer size reached', done => { - const clock = sinon.useFakeTimers(); - const processor = new BatchLogRecordProcessor( - exporter, - defaultBufferConfig - ); - for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { - const logRecord = createLogRecord(); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - processor.onEmit(logRecord); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - } - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - setTimeout(async () => { - assert.strictEqual( - exporter.getFinishedLogRecords().length, - defaultBufferConfig.maxExportBatchSize - ); - await processor.shutdown(); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - done(); - }, defaultBufferConfig.scheduledDelayMillis + 1000); - clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); - clock.restore(); - }); - - it('should force flush when timeout exceeded', done => { - const clock = sinon.useFakeTimers(); - const processor = new BatchLogRecordProcessor( - exporter, - defaultBufferConfig - ); - for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - } - setTimeout(() => { - assert.strictEqual( - exporter.getFinishedLogRecords().length, - defaultBufferConfig.maxExportBatchSize - ); - done(); - }, defaultBufferConfig.scheduledDelayMillis + 1000); - clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); - clock.restore(); - }); - - it('should not export empty log record lists', done => { - const spy = sinon.spy(exporter, 'export'); - const clock = sinon.useFakeTimers(); - new BatchLogRecordProcessor(exporter, defaultBufferConfig); - setTimeout(() => { - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - sinon.assert.notCalled(spy); - done(); - }, defaultBufferConfig.scheduledDelayMillis + 1000); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); - - clock.restore(); - }); - - it('should export each log record exactly once with buffer size reached multiple times', done => { - const originalTimeout = setTimeout; - const clock = sinon.useFakeTimers(); - const processor = new BatchLogRecordProcessor( - exporter, - defaultBufferConfig - ); - const totalLogRecords = defaultBufferConfig.maxExportBatchSize * 2; - for (let i = 0; i < totalLogRecords; i++) { - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - } - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); - originalTimeout(() => { - clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); - originalTimeout(async () => { - clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); - clock.restore(); - assert.strictEqual( - exporter.getFinishedLogRecords().length, - totalLogRecords + 1 - ); - await processor.shutdown(); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - done(); - }); - }); - }); - - it('should call globalErrorHandler when exporting fails', done => { - const clock = sinon.useFakeTimers(); - const expectedError = new Error('Exporter failed'); - sinon.stub(exporter, 'export').callsFake((_, callback) => { - setTimeout(() => { - callback({ code: ExportResultCode.FAILED, error: expectedError }); - }, 0); - }); - const errorHandlerSpy = sinon.spy(); - setGlobalErrorHandler(errorHandlerSpy); - const processor = new BatchLogRecordProcessor( - exporter, - defaultBufferConfig - ); - for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - } - clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); - clock.restore(); - setTimeout(() => { - assert.strictEqual(errorHandlerSpy.callCount, 1); - const [[error]] = errorHandlerSpy.args; - assert.deepStrictEqual(error, expectedError); - // reset global error handler - setGlobalErrorHandler(loggingErrorHandler()); - done(); - }); - }); - - it('should drop logRecords when there are more logRecords then "maxQueueSize"', () => { - const maxQueueSize = 6; - const processor = new BatchLogRecordProcessor(exporter, { maxQueueSize }); - const logRecord = createLogRecord(); - for (let i = 0; i < maxQueueSize + 10; i++) { - processor.onEmit(logRecord); - } - // @ts-expect-error - assert.strictEqual(processor._finishedLogRecords.length, 6); - }); - }); - - describe('forceFlush', () => { - it('should force flush on demand', () => { - const processor = new BatchLogRecordProcessor( - exporter, - defaultBufferConfig - ); - for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - } - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - processor.forceFlush(); - assert.strictEqual( - exporter.getFinishedLogRecords().length, - defaultBufferConfig.maxExportBatchSize - ); - }); - - it('should call an async callback when flushing is complete', async () => { - const processor = new BatchLogRecordProcessor(exporter); - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - await processor.forceFlush(); - assert.strictEqual(exporter.getFinishedLogRecords().length, 1); - }); - }); - - describe('shutdown', () => { - it('should call onShutdown', async () => { - const processor = new BatchLogRecordProcessor(exporter); - const onShutdownSpy = sinon.stub(processor, 'onShutdown'); - assert.strictEqual(onShutdownSpy.callCount, 0); - await processor.shutdown(); - assert.strictEqual(onShutdownSpy.callCount, 1); - }); - - it('should call an async callback when shutdown is complete', async () => { - let exportedLogRecords = 0; - sinon.stub(exporter, 'export').callsFake((logRecords, callback) => { - setTimeout(() => { - exportedLogRecords = exportedLogRecords + logRecords.length; - callback({ code: ExportResultCode.SUCCESS }); - }, 0); - }); - const processor = new BatchLogRecordProcessor(exporter); - const logRecord = createLogRecord(); - processor.onEmit(logRecord); - await processor.shutdown(); - assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - assert.strictEqual(exportedLogRecords, 1); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts deleted file mode 100644 index a9f3a3e2ac..0000000000 --- a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.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 * as assert from 'assert'; -import * as sinon from 'sinon'; - -import type { ReadableLogRecord } from './../../../src'; -import { ConsoleLogRecordExporter } from '../../../src'; - -describe('ConsoleLogRecordExporter', () => { - let previousConsoleDir: typeof console.dir; - - beforeEach(() => { - previousConsoleDir = console.dir; - console.dir = () => {}; - }); - - afterEach(() => { - console.dir = previousConsoleDir; - }); - - describe('export', () => { - it('should export information about log record', done => { - const consoleExporter = new ConsoleLogRecordExporter(); - const spyConsole = sinon.spy(console, 'dir'); - const logs: ReadableLogRecord[] = []; - for (let i = 0; i < 10; i++) { - // @ts-expect-error - logs.push({ time: [0, 0] }); - } - consoleExporter.export(logs, () => { - assert.strictEqual(spyConsole.callCount, logs.length); - done(); - }); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts deleted file mode 100644 index 1b8cdff27a..0000000000 --- a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts +++ /dev/null @@ -1,100 +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 type { ReadableLogRecord } from '../../../src'; -import { InMemoryLogRecordExporter } from '../../../src'; - -describe('InMemoryLogRecordExporter', () => { - describe('export', () => { - it('should export information about log record', done => { - const memoryExporter = new InMemoryLogRecordExporter(); - const logs: ReadableLogRecord[] = []; - for (let i = 0; i < 10; i++) { - // @ts-expect-error - logs.push({}); - } - memoryExporter.export(logs, () => { - assert.strictEqual( - memoryExporter.getFinishedLogRecords().length, - logs.length - ); - done(); - }); - }); - }); - - describe('shutdown', () => { - it('should clear all log records', done => { - const memoryExporter = new InMemoryLogRecordExporter(); - const logs: ReadableLogRecord[] = []; - for (let i = 0; i < 10; i++) { - // @ts-expect-error - logs.push({}); - } - memoryExporter.export(logs, () => { - assert.strictEqual( - memoryExporter.getFinishedLogRecords().length, - logs.length - ); - memoryExporter.shutdown(); - // @ts-expect-error - assert.strictEqual(memoryExporter._finishedLogRecords.length, 0); - done(); - }); - }); - }); - - describe('getFinishedLogRecords', () => { - it('should get all log records', done => { - const memoryExporter = new InMemoryLogRecordExporter(); - const logs: ReadableLogRecord[] = []; - for (let i = 0; i < 10; i++) { - // @ts-expect-error - logs.push({}); - } - memoryExporter.export(logs, () => { - assert.strictEqual( - memoryExporter.getFinishedLogRecords().length, - logs.length - ); - done(); - }); - }); - }); - - describe('reset', () => { - it('should clear all log records', done => { - const memoryExporter = new InMemoryLogRecordExporter(); - const logs: ReadableLogRecord[] = []; - for (let i = 0; i < 10; i++) { - // @ts-expect-error - logs.push({}); - } - memoryExporter.export(logs, () => { - assert.strictEqual( - memoryExporter.getFinishedLogRecords().length, - logs.length - ); - memoryExporter.reset(); - // @ts-expect-error - assert.strictEqual(memoryExporter._finishedLogRecords.length, 0); - done(); - }); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts deleted file mode 100644 index 2c4c3fd1d6..0000000000 --- a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts +++ /dev/null @@ -1,88 +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 * as sinon from 'sinon'; -import { - ExportResultCode, - loggingErrorHandler, - setGlobalErrorHandler, -} from '@opentelemetry/core'; - -import type { LogRecordExporter, ReadableLogRecord } from './../../../src'; -import { SimpleLogRecordProcessor } from './../../../src'; - -const setup = (exporter?: LogRecordExporter) => { - // @ts-expect-error - const logRecordExporter: LogRecordExporter = exporter || {}; - const processor = new SimpleLogRecordProcessor(logRecordExporter); - return { processor }; -}; - -describe('SimpleLogRecordProcessor', () => { - describe('constructor', () => { - it('should create a SimpleLogRecordProcessor instance', () => { - assert.ok(setup().processor instanceof SimpleLogRecordProcessor); - }); - }); - - describe('onEmit', () => { - it('should handle onEmit', async () => { - const exportSpy = sinon.spy(); - // @ts-expect-error - const { processor } = setup({ export: exportSpy }); - // @ts-expect-error - const logRecord: ReadableLogRecord = {}; - processor.onEmit(logRecord); - assert.ok(exportSpy.callCount === 1); - }); - - it('should call globalErrorHandler when exporting fails', async () => { - const expectedError = new Error('Exporter failed'); - // @ts-expect-error - const exporter: LogRecordExporter = { - export: (_, callback) => - setTimeout( - () => - callback({ code: ExportResultCode.FAILED, error: expectedError }), - 0 - ), - }; - const { processor } = setup(exporter); - // @ts-expect-error - const logRecord: ReadableLogRecord = {}; - const errorHandlerSpy = sinon.spy(); - setGlobalErrorHandler(errorHandlerSpy); - processor.onEmit(logRecord); - await new Promise(resolve => setTimeout(() => resolve(), 0)); - assert.strictEqual(errorHandlerSpy.callCount, 1); - const [[error]] = errorHandlerSpy.args; - assert.deepStrictEqual(error, expectedError); - // reset global error handler - setGlobalErrorHandler(loggingErrorHandler()); - }); - }); - - describe('shutdown', () => { - it('should handle shutdown', async () => { - const shutdownSpy = sinon.spy(); - // @ts-expect-error - const { processor } = setup({ shutdown: shutdownSpy }); - processor.shutdown(); - assert.ok(shutdownSpy.callCount === 1); - }); - }); -}); diff --git a/experimental/packages/sdk-logs/test/common/utils.ts b/experimental/packages/sdk-logs/test/common/utils.ts deleted file mode 100644 index fd801fc30d..0000000000 --- a/experimental/packages/sdk-logs/test/common/utils.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. - */ - -export const validAttributes = { - string: 'string', - number: 0, - bool: true, - 'array': ['str1', 'str2'], - 'array': [1, 2], - 'array': [true, false], -}; - -export const invalidAttributes = { - // invalid attribute type object - object: { foo: 'bar' }, - // invalid attribute inhomogeneous array - 'non-homogeneous-array': [0, ''], - // This empty length attribute should not be set - '': 'empty-key', -}; From 097d15d92070584f531546e50588369af72ecd56 Mon Sep 17 00:00:00 2001 From: fugao Date: Sat, 18 Feb 2023 15:17:06 +0800 Subject: [PATCH 11/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 3eccb0da14..56b0dc31e1 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-logs", - "version": "0.35.0", + "version": "0.35.1", "publishConfig": { "access": "public" }, From 1e1263b6bb6eb52d9ca87d3243beeea037e99726 Mon Sep 17 00:00:00 2001 From: fugao Date: Sat, 18 Feb 2023 15:23:49 +0800 Subject: [PATCH 12/44] feat(sdk-logs): sdk-logs init --- .../sdk-logs/src/export/ConsoleLogRecordExporter.ts | 11 +++++++---- .../browser/export/BatchLogRecordProcessor.ts | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts index 7e1a40679f..bff34a9ec5 100644 --- a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts +++ b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts @@ -24,6 +24,8 @@ import type { LogRecordExporter } from './LogRecordExporter'; * This is implementation of {@link LogRecordExporter} that prints LogRecords to the * console. This class can be used for diagnostic purposes. */ + +/* eslint-disable no-console */ export class ConsoleLogRecordExporter implements LogRecordExporter { /** * Export logs. @@ -34,7 +36,7 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { logs: ReadableLogRecord[], resultCallback: (result: ExportResult) => void ) { - this._sendLogRecords(logs).then(res => resultCallback(res)); + this._sendLogRecords(logs, resultCallback); } /** @@ -67,11 +69,12 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { * @param done */ private _sendLogRecords( - logRecords: ReadableLogRecord[] - ): Promise { + logRecords: ReadableLogRecord[], + done?: (result: ExportResult) => void + ): void { for (const logRecord of logRecords) { console.dir(this._exportInfo(logRecord), { depth: 3 }); } - return Promise.resolve({ code: ExportResultCode.SUCCESS }); + done?.({ code: ExportResultCode.SUCCESS }); } } diff --git a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts index cb268bb94b..8437834375 100644 --- a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts @@ -54,11 +54,11 @@ export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { if (document.visibilityState === 'hidden') { - this.forceFlush(); + void this.forceFlush(); } }; this._pageHideListener = () => { - this.forceFlush(); + void this.forceFlush(); }; document.addEventListener( 'visibilitychange', From 66090b04304f805bef6a273f9413aaf6ea2c70e3 Mon Sep 17 00:00:00 2001 From: fugao Date: Sat, 18 Feb 2023 18:27:32 +0800 Subject: [PATCH 13/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/package.json | 2 +- .../packages/sdk-logs/src/LogRecord.ts | 12 +- experimental/packages/sdk-logs/src/Logger.ts | 6 +- .../packages/sdk-logs/src/LoggerProvider.ts | 88 ++++- .../sdk-logs/src/MultiLogRecordProcessor.ts | 19 +- .../src/export/NoopLogRecordProcessor.ts | 30 ++ experimental/packages/sdk-logs/src/index.ts | 1 + .../test/common/LoggerProvider.test.ts | 314 ++++++++++++++++++ .../src/utils/environment.ts | 2 + 9 files changed, 445 insertions(+), 29 deletions(-) create mode 100644 experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts create mode 100644 experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 56b0dc31e1..5f18460d54 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -87,7 +87,7 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.4.0" + "@opentelemetry/api": "^1.4.0" }, "dependencies": { "@opentelemetry/core": "1.9.1", diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 9ca5310894..28f5b37c48 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -43,7 +43,7 @@ export class LogRecord implements ReadableLogRecord { private _isEmitted = false; constructor( - private readonly config: LoggerConfig, + private readonly _config: LoggerConfig, logRecord: logsAPI.LogRecord ) { const { @@ -64,8 +64,8 @@ export class LogRecord implements ReadableLogRecord { this.severityNumber = severityNumber; this.severityText = severityText; this.body = body; - this.resource = this.config.resource; - this.instrumentationScope = this.config.instrumentationScope; + this.resource = this._config.resource; + this.instrumentationScope = this._config.instrumentationScope; this.setAttributes(attributes); } @@ -74,7 +74,7 @@ export class LogRecord implements ReadableLogRecord { return; } this._isEmitted = true; - this.config.activeProcessor.onEmit(this); + this._config.activeProcessor.onEmit(this); } public setAttribute(key: string, value?: AttributeValue) { @@ -91,7 +91,7 @@ export class LogRecord implements ReadableLogRecord { } if ( Object.keys(this.attributes).length >= - this.config.logRecordLimits.attributeCountLimit! && + this._config.logRecordLimits.attributeCountLimit! && !Object.prototype.hasOwnProperty.call(this.attributes, key) ) { return; @@ -113,7 +113,7 @@ export class LogRecord implements ReadableLogRecord { } private _truncateToSize(value: AttributeValue): AttributeValue { - const limit = this.config.logRecordLimits.attributeValueLengthLimit || 0; + const limit = this._config.logRecordLimits.attributeValueLengthLimit || 0; // Check limit if (limit <= 0) { // Negative values are invalid, so do not truncate diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index 1111f82584..8e7d1e7ed0 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -16,7 +16,7 @@ import type * as logsAPI from '@opentelemetry/api-logs'; -import type { LoggerConfig } from './types'; +import type { LoggerConfig, LogRecordLimits } from './types'; import { LogRecord } from './LogRecord'; export class Logger implements logsAPI.Logger { @@ -25,4 +25,8 @@ export class Logger implements logsAPI.Logger { public emit(logRecord: logsAPI.LogRecord): void { new LogRecord(this._config, logRecord).emit(); } + + public getLogRecordLimits(): LogRecordLimits { + return this._config.logRecordLimits; + } } diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index d12055c561..0ab2040e17 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -13,22 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { diag } from '@opentelemetry/api'; import type * as logsAPI from '@opentelemetry/api-logs'; import { IResource, Resource } from '@opentelemetry/resources'; -import { merge } from '@opentelemetry/core'; +import { getEnv, merge } from '@opentelemetry/core'; import type { LoggerProviderConfig, LogRecordLimits } from './types'; import type { LogRecordProcessor } from './LogRecordProcessor'; +import type { LogRecordExporter } from './export/LogRecordExporter'; import { Logger } from './Logger'; import { loadDefaultConfig } from './config'; import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; +import { BatchLogRecordProcessor } from './platform/node/export/BatchLogRecordProcessor'; +import { NoopLogRecordProcessor } from './export/NoopLogRecordProcessor'; + +export type EXPORTER_FACTORY = () => LogRecordExporter; export class LoggerProvider implements logsAPI.LoggerProvider { + protected static readonly _registeredExporters = new Map< + string, + EXPORTER_FACTORY + >(); + + public readonly resource: IResource; + private readonly _loggers: Map = new Map(); - private readonly _resource: IResource; private readonly _logRecordLimits: LogRecordLimits; - private readonly _activeProcessor: MultiLogRecordProcessor; + private _activeProcessor: MultiLogRecordProcessor; + private readonly _registeredLogRecordProcessors: LogRecordProcessor[] = []; + private readonly _forceFlushTimeoutMillis; constructor(config: LoggerProviderConfig = {}) { const { resource, logRecordLimits, forceFlushTimeoutMillis } = merge( @@ -36,11 +49,22 @@ export class LoggerProvider implements logsAPI.LoggerProvider { loadDefaultConfig(), config ); - this._resource = Resource.default().merge(resource ?? Resource.empty()); - this._activeProcessor = new MultiLogRecordProcessor( - forceFlushTimeoutMillis - ); + this.resource = Resource.default().merge(resource ?? Resource.empty()); this._logRecordLimits = logRecordLimits; + this._forceFlushTimeoutMillis = forceFlushTimeoutMillis; + + const defaultExporter = this._buildExporterFromEnv(); + if (defaultExporter !== undefined) { + this._activeProcessor = new MultiLogRecordProcessor( + [new BatchLogRecordProcessor(defaultExporter)], + forceFlushTimeoutMillis + ); + } else { + this._activeProcessor = new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ); + } } /** @@ -57,7 +81,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { this._loggers.set( key, new Logger({ - resource: this._resource, + resource: this.resource, logRecordLimits: this._logRecordLimits, activeProcessor: this._activeProcessor, instrumentationScope: { name, version, schemaUrl }, @@ -72,7 +96,23 @@ export class LoggerProvider implements logsAPI.LoggerProvider { * @param processor the new LogRecordProcessor to be added. */ public addLogRecordProcessor(processor: LogRecordProcessor) { - this._activeProcessor.addLogRecordProcessor(processor); + if (this._registeredLogRecordProcessors.length === 0) { + // since we might have enabled by default a batchProcessor, we disable it + // before adding the new one + this._activeProcessor + .shutdown() + .catch(err => + diag.error( + 'Error while trying to shutdown current log record processor', + err + ) + ); + } + this._registeredLogRecordProcessors.push(processor); + this._activeProcessor = new MultiLogRecordProcessor( + this._registeredLogRecordProcessors, + this._forceFlushTimeoutMillis + ); } /** @@ -93,4 +133,32 @@ export class LoggerProvider implements logsAPI.LoggerProvider { public shutdown(): Promise { return this._activeProcessor.shutdown(); } + + public getActiveLogRecordProcessor(): MultiLogRecordProcessor { + return this._activeProcessor; + } + + public getActiveLoggers(): Map { + return this._loggers; + } + + protected _getLogRecordExporter(name: string): LogRecordExporter | undefined { + return (this.constructor as typeof LoggerProvider)._registeredExporters.get( + name + )?.(); + } + + protected _buildExporterFromEnv(): LogRecordExporter | undefined { + const exporterName = getEnv().OTEL_LOGS_EXPORTER; + if (exporterName === 'none' || exporterName === '') { + return; + } + const exporter = this._getLogRecordExporter(exporterName); + if (!exporter) { + diag.error( + `Exporter "${exporterName}" requested through environment variable is unavailable.` + ); + } + return exporter; + } } diff --git a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts index b46cd31b4c..9b09fff731 100644 --- a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts @@ -24,28 +24,25 @@ import type { ReadableLogRecord } from './export/ReadableLogRecord'; * received events to a list of {@link LogRecordProcessor}s. */ export class MultiLogRecordProcessor implements LogRecordProcessor { - private readonly _processors: LogRecordProcessor[] = []; - - constructor(private readonly _forceFlushTimeoutMillis: number) {} - - public addLogRecordProcessor(processor: LogRecordProcessor) { - this._processors.push(processor); - } + constructor( + public readonly processors: LogRecordProcessor[], + public readonly forceFlushTimeoutMillis: number + ) {} public async forceFlush(): Promise { - const timeout = this._forceFlushTimeoutMillis; + const timeout = this.forceFlushTimeoutMillis; await Promise.all( - this._processors.map(processor => + this.processors.map(processor => callWithTimeout(processor.forceFlush(), timeout) ) ); } public onEmit(logRecord: ReadableLogRecord): void { - this._processors.forEach(processors => processors.onEmit(logRecord)); + this.processors.forEach(processors => processors.onEmit(logRecord)); } public async shutdown(): Promise { - await Promise.all(this._processors.map(processor => processor.shutdown())); + await Promise.all(this.processors.map(processor => processor.shutdown())); } } diff --git a/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts new file mode 100644 index 0000000000..91f277e8ab --- /dev/null +++ b/experimental/packages/sdk-logs/src/export/NoopLogRecordProcessor.ts @@ -0,0 +1,30 @@ +/* + * 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 { LogRecordProcessor } from '../LogRecordProcessor'; +import { ReadableLogRecord } from './ReadableLogRecord'; + +export class NoopLogRecordProcessor implements LogRecordProcessor { + forceFlush(): Promise { + return Promise.resolve(); + } + + onEmit(_logRecord: ReadableLogRecord): void {} + + shutdown(): Promise { + return Promise.resolve(); + } +} diff --git a/experimental/packages/sdk-logs/src/index.ts b/experimental/packages/sdk-logs/src/index.ts index 1d5de184dd..a119b0a362 100644 --- a/experimental/packages/sdk-logs/src/index.ts +++ b/experimental/packages/sdk-logs/src/index.ts @@ -20,6 +20,7 @@ export * from './Logger'; export * from './LogRecord'; export * from './LogRecordProcessor'; export * from './export/ReadableLogRecord'; +export * from './export/NoopLogRecordProcessor'; export * from './export/BatchLogRecordProcessor'; export * from './export/ConsoleLogRecordExporter'; export * from './export/LogRecordExporter'; diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts new file mode 100644 index 0000000000..0880dda30f --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -0,0 +1,314 @@ +/* + * 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 { logs } from '@opentelemetry/api-logs'; +import { diag } from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; + +import { Logger, LoggerProvider, NoopLogRecordProcessor } from '../../src'; +import { loadDefaultConfig } from '../../src/config'; + +describe('LoggerProvider', () => { + let envSource: Record; + + if (typeof process === 'undefined') { + envSource = globalThis as unknown as Record; + } else { + envSource = process.env as Record; + } + + beforeEach(() => { + // to avoid actually registering the LoggerProvider and leaking env to other tests + sinon.stub(logs, 'setGlobalLoggerProvider'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('constructor', () => { + describe('when options not defined', () => { + it('should construct an instance', () => { + const provider = new LoggerProvider(); + assert.ok(provider instanceof LoggerProvider); + }); + + it('should use noop span processor by default and no diag error', () => { + const errorStub = sinon.spy(diag, 'error'); + const provider = new LoggerProvider(); + const processors = provider.getActiveLogRecordProcessor().processors; + assert.ok(processors.length === 1); + assert.ok(processors[0] instanceof NoopLogRecordProcessor); + sinon.assert.notCalled(errorStub); + }); + + it('should have default resource if not pass', () => { + const provider = new LoggerProvider(); + const { resource } = provider; + assert.deepStrictEqual(resource, Resource.default()); + }); + + it('should have default forceFlushTimeoutMillis if not pass', () => { + const provider = new LoggerProvider(); + const activeProcessor = provider.getActiveLogRecordProcessor(); + assert.ok( + activeProcessor.forceFlushTimeoutMillis === + loadDefaultConfig().forceFlushTimeoutMillis + ); + }); + }); + + describe('when user sets unavailable exporter', () => { + it('should use noop log record processor by default and show diag error', () => { + const errorStub = sinon.spy(diag, 'error'); + envSource.OTEL_LOGS_EXPORTER = 'someExporter'; + + const provider = new LoggerProvider(); + const processors = provider.getActiveLogRecordProcessor().processors; + assert.ok(processors.length === 1); + assert.ok(processors[0] instanceof NoopLogRecordProcessor); + + sinon.assert.calledWith( + errorStub, + 'Exporter "someExporter" requested through environment variable is unavailable.' + ); + delete envSource.OTEL_LOGS_EXPORTER; + }); + }); + + describe('logRecordLimits', () => { + describe('when not defined default values', () => { + it('should have logger with default values', () => { + const logger = new LoggerProvider({}).getLogger('default'); + assert.deepStrictEqual(logger.getLogRecordLimits(), { + attributeValueLengthLimit: Infinity, + attributeCountLimit: 128, + }); + }); + }); + + describe('when "attributeCountLimit" is defined', () => { + it('should have logger with defined value', () => { + const logger = new LoggerProvider({ + logRecordLimits: { + attributeCountLimit: 100, + }, + }).getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual(logRecordLimits.attributeCountLimit, 100); + }); + }); + + describe('when "attributeValueLengthLimit" is defined', () => { + it('should have logger with defined value', () => { + const logger = new LoggerProvider({ + logRecordLimits: { + attributeValueLengthLimit: 10, + }, + }).getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual(logRecordLimits.attributeValueLengthLimit, 10); + }); + + it('should have logger with negative "attributeValueLengthLimit" value', () => { + const logger = new LoggerProvider({ + logRecordLimits: { + attributeValueLengthLimit: -10, + }, + }).getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual(logRecordLimits.attributeValueLengthLimit, -10); + }); + }); + + describe('when attribute value length limit is defined via env', () => { + it('should have span attribute value length limit as deafult of Infinity', () => { + envSource.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'Infinity'; + const logger = new LoggerProvider().getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual( + logRecordLimits.attributeValueLengthLimit, + Infinity + ); + delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT; + }); + }); + + describe('when attribute value length limit is not defined via env', () => { + it('should use default value of Infinity', () => { + const logger = new LoggerProvider().getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual( + logRecordLimits.attributeValueLengthLimit, + Infinity + ); + }); + }); + + describe('when attribute count limit is defined via env', () => { + it('should have span and general attribute count limits as defined in env', () => { + envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT = '35'; + const logger = new LoggerProvider().getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual(logRecordLimits.attributeCountLimit, 35); + delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT; + }); + it('should have span attribute count limit as default of 128', () => { + envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT = '128'; + const logger = new LoggerProvider().getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual(logRecordLimits.attributeCountLimit, 128); + delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT; + }); + }); + + describe('when attribute count limit is not defined via env', () => { + it('should use default value of 128', () => { + const logger = new LoggerProvider().getLogger('default'); + const logRecordLimits = logger.getLogRecordLimits(); + assert.strictEqual(logRecordLimits.attributeCountLimit, 128); + }); + }); + }); + }); + + describe('getLogger', () => { + const testName = 'test name'; + const testVersion = 'test version'; + const testSchemaURL = 'test schema url'; + + it("should create a logger instance if the name doesn't exist", () => { + const provider = new LoggerProvider(); + assert.ok(provider.getActiveLoggers().size === 0); + provider.getLogger(testName); + assert.ok(provider.getActiveLoggers().size === 1); + }); + + it('should create A new object if the name & version & schemaUrl are not unique', () => { + const provider = new LoggerProvider(); + assert.ok(provider.getActiveLoggers().size === 0); + + provider.getLogger(testName); + assert.ok(provider.getActiveLoggers().size === 1); + provider.getLogger(testName, testVersion); + assert.ok(provider.getActiveLoggers().size === 2); + provider.getLogger(testName, testVersion, { schemaUrl: testSchemaURL }); + assert.ok(provider.getActiveLoggers().size === 3); + }); + + it('should not create A new object if the name & version & schemaUrl are unique', () => { + const provider = new LoggerProvider(); + + assert.ok(provider.getActiveLoggers().size === 0); + provider.getLogger(testName); + assert.ok(provider.getActiveLoggers().size === 1); + const logger1 = provider.getLogger(testName, testVersion, { + schemaUrl: testSchemaURL, + }); + assert.ok(provider.getActiveLoggers().size === 2); + const logger2 = provider.getLogger(testName, testVersion, { + schemaUrl: testSchemaURL, + }); + assert.ok(provider.getActiveLoggers().size === 2); + assert.ok(logger2 instanceof Logger); + assert.ok(logger1 === logger2); + }); + }); + + describe('addLogRecordProcessor', () => { + it('should add logRecord processor', () => { + const logRecordProcessor = new NoopLogRecordProcessor(); + const provider = new LoggerProvider(); + provider.addLogRecordProcessor(logRecordProcessor); + assert.strictEqual( + provider.getActiveLogRecordProcessor().processors.length, + 1 + ); + }); + }); + + describe('.forceFlush()', () => { + it('should call forceFlush on all registered log record processors', done => { + sinon.restore(); + const forceFlushStub = sinon.stub( + NoopLogRecordProcessor.prototype, + 'forceFlush' + ); + forceFlushStub.resolves(); + + const provider = new LoggerProvider(); + const logRecordProcessorOne = new NoopLogRecordProcessor(); + const logRecordProcessorTwo = new NoopLogRecordProcessor(); + + provider.addLogRecordProcessor(logRecordProcessorOne); + provider.addLogRecordProcessor(logRecordProcessorTwo); + + provider + .forceFlush() + .then(() => { + sinon.restore(); + assert(forceFlushStub.calledTwice); + done(); + }) + .catch(error => { + sinon.restore(); + done(error); + }); + }); + + it('should throw error when calling forceFlush on all registered span processors fails', done => { + sinon.restore(); + + const forceFlushStub = sinon.stub( + NoopLogRecordProcessor.prototype, + 'forceFlush' + ); + forceFlushStub.returns(Promise.reject('Error')); + + const provider = new LoggerProvider(); + const logRecordProcessorOne = new NoopLogRecordProcessor(); + const logRecordProcessorTwo = new NoopLogRecordProcessor(); + + provider.addLogRecordProcessor(logRecordProcessorOne); + provider.addLogRecordProcessor(logRecordProcessorTwo); + + provider + .forceFlush() + .then(() => { + sinon.restore(); + done(new Error('Successful forceFlush not expected')); + }) + .catch(_error => { + sinon.restore(); + sinon.assert.calledTwice(forceFlushStub); + done(); + }); + }); + }); + + describe('.shutdown()', () => { + it('should trigger shutdown when manually invoked', () => { + const provider = new LoggerProvider(); + const shutdownStub = sinon.stub( + provider.getActiveLogRecordProcessor(), + 'shutdown' + ); + provider.shutdown(); + sinon.assert.calledOnce(shutdownStub); + }); + }); +}); diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index eecd13f486..634df32135 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -102,6 +102,7 @@ export type ENVIRONMENT = { OTEL_TRACES_EXPORTER?: string; OTEL_TRACES_SAMPLER_ARG?: string; OTEL_TRACES_SAMPLER?: string; + OTEL_LOGS_EXPORTER?: string; OTEL_EXPORTER_OTLP_INSECURE?: string; OTEL_EXPORTER_OTLP_TRACES_INSECURE?: string; OTEL_EXPORTER_OTLP_METRICS_INSECURE?: string; @@ -180,6 +181,7 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_TRACES_EXPORTER: '', OTEL_TRACES_SAMPLER: TracesSamplerValues.ParentBasedAlwaysOn, OTEL_TRACES_SAMPLER_ARG: '', + OTEL_LOGS_EXPORTER: '', OTEL_EXPORTER_OTLP_INSECURE: '', OTEL_EXPORTER_OTLP_TRACES_INSECURE: '', OTEL_EXPORTER_OTLP_METRICS_INSECURE: '', From f045cf67e6cc159cf0c8f5b1310f403eb1609f6e Mon Sep 17 00:00:00 2001 From: fugao Date: Sun, 26 Feb 2023 21:19:43 +0800 Subject: [PATCH 14/44] feat(sdk-logs): sdk-logs init --- .../packages/sdk-logs/src/LogRecord.ts | 4 + .../sdk-logs/test/common/LogRecord.test.ts | 314 ++++++++++++++++++ .../sdk-logs/test/common/Logger.test.ts | 61 ++++ .../common/MultiLogRecordProcessor.test.ts | 213 ++++++++++++ .../export/ConsoleLogRecordExporter.test.ts | 80 +++++ .../export/InMemoryLogRecordExporter.test.ts | 94 ++++++ .../export/SimpleLogRecordProcessor.test.ts | 139 ++++++++ .../packages/sdk-logs/test/common/utils.ts | 33 ++ 8 files changed, 938 insertions(+) create mode 100644 experimental/packages/sdk-logs/test/common/LogRecord.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/Logger.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/utils.ts diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 28f5b37c48..48cd33d2d6 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -105,6 +105,10 @@ export class LogRecord implements ReadableLogRecord { } } + get emitted(): boolean { + return this._isEmitted; + } + private _isLogRecordEmitted(): boolean { if (this._isEmitted) { api.diag.warn('Can not execute the operation on emitted LogRecord'); diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts new file mode 100644 index 0000000000..7e3b58d848 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts @@ -0,0 +1,314 @@ +/* + * 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 type { Attributes, AttributeValue } from '@opentelemetry/api'; +import * as logsAPI from '@opentelemetry/api-logs'; +import type { HrTime } from '@opentelemetry/api'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Resource } from '@opentelemetry/resources'; +import { hrTimeToMilliseconds, timeInputToHrTime } from '@opentelemetry/core'; + +import { + LogRecordLimits, + LogRecordProcessor, + NoopLogRecordProcessor, + LogRecord, + LoggerProvider, +} from './../../src'; +import { loadDefaultConfig } from '../../src/config'; +import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; +import { invalidAttributes, validAttributes } from './utils'; + +const performanceTimeOrigin: HrTime = [1, 1]; + +const setup = (limits?: LogRecordLimits, data?: logsAPI.LogRecord) => { + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const config = { + activeProcessor: new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ), + resource: Resource.default(), + logRecordLimits: { + attributeValueLengthLimit: + limits?.attributeValueLengthLimit ?? + logRecordLimits.attributeValueLengthLimit, + attributeCountLimit: + limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, + }, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }; + const logRecord = new LogRecord(config, data || {}); + return { logRecord, config }; +}; + +describe('LogRecord', () => { + describe('constructor', () => { + it('should create an instance', () => { + const { logRecord } = setup(); + assert.ok(logRecord instanceof LogRecord); + }); + + it('should have a default timestamp', () => { + const { logRecord } = setup(); + assert.ok(logRecord.time !== undefined); + assert.ok( + hrTimeToMilliseconds(logRecord.time) > + hrTimeToMilliseconds(performanceTimeOrigin) + ); + }); + + it('should have a default timestamp', () => { + const { logRecord } = setup(); + assert.ok(logRecord.time !== undefined); + assert.ok( + hrTimeToMilliseconds(logRecord.time) > + hrTimeToMilliseconds(performanceTimeOrigin) + ); + }); + + it('should return ReadableLogRecord', () => { + const logRecordData: logsAPI.LogRecord = { + timestamp: new Date().getTime(), + severityNumber: logsAPI.SeverityNumber.DEBUG, + severityText: 'DEBUG', + body: 'this is a body', + attributes: { + name: 'test name', + }, + traceId: 'trance id', + spanId: 'span id', + traceFlags: 1, + }; + const { logRecord, config } = setup(undefined, logRecordData); + assert.deepStrictEqual( + logRecord.time, + timeInputToHrTime(logRecordData.timestamp!) + ); + assert.strictEqual( + logRecord.severityNumber, + logRecordData.severityNumber + ); + assert.strictEqual(logRecord.severityText, logRecordData.severityText); + assert.strictEqual(logRecord.body, logRecordData.body); + assert.deepStrictEqual(logRecord.attributes, logRecordData.attributes); + assert.deepStrictEqual(logRecord.traceId, logRecordData.traceId); + assert.deepStrictEqual(logRecord.spanId, logRecordData.spanId); + assert.deepStrictEqual(logRecord.traceFlags, logRecordData.traceFlags); + assert.deepStrictEqual(logRecord.resource, config.resource); + assert.deepStrictEqual( + logRecord.instrumentationScope, + config.instrumentationScope + ); + }); + + it('should return ReadableLogRecord with attributes', () => { + const logRecordData: logsAPI.LogRecord = { + timestamp: new Date().getTime(), + severityNumber: logsAPI.SeverityNumber.DEBUG, + severityText: 'DEBUG', + body: 'this is a body', + attributes: { + name: 'test name', + }, + traceId: 'trance id', + spanId: 'span id', + traceFlags: 1, + }; + const { logRecord } = setup(undefined, logRecordData); + + assert.deepStrictEqual(logRecord.attributes, { name: 'test name' }); + + logRecord.setAttribute('attr1', 'value1'); + assert.deepStrictEqual(logRecord.attributes, { + name: 'test name', + attr1: 'value1', + }); + + logRecord.setAttributes({ attr2: 123, attr1: false }); + assert.deepStrictEqual(logRecord.attributes, { + name: 'test name', + attr1: false, + attr2: 123, + }); + + logRecord.emit(); + // shouldn't add new attribute + logRecord.setAttribute('attr3', 'value3'); + assert.deepStrictEqual(logRecord.attributes, { + name: 'test name', + attr1: false, + attr2: 123, + }); + }); + }); + + describe('setAttribute', () => { + describe('when default options set', () => { + it('should set an attribute', () => { + const { logRecord } = setup(); + for (const [k, v] of Object.entries(validAttributes)) { + logRecord.setAttribute(k, v); + } + for (const [k, v] of Object.entries(invalidAttributes)) { + logRecord.setAttribute(k, v as unknown as AttributeValue); + } + assert.deepStrictEqual(logRecord.attributes, validAttributes); + }); + + it('should be able to overwrite attributes', () => { + const { logRecord } = setup(); + logRecord.setAttribute('overwrite', 'initial value'); + logRecord.setAttribute('overwrite', 'overwritten value'); + assert.deepStrictEqual(logRecord.attributes, { + overwrite: 'overwritten value', + }); + }); + }); + + describe('when logRecordLimits options set', () => { + describe('when "attributeCountLimit" option defined', () => { + const { logRecord } = setup({ attributeCountLimit: 100 }); + for (let i = 0; i < 150; i++) { + logRecord.setAttribute(`foo${i}`, `bar${i}`); + } + + it('should remove / drop all remaining values after the number of values exceeds this limit', () => { + const { attributes } = logRecord; + assert.strictEqual(Object.keys(attributes).length, 100); + assert.strictEqual(attributes.foo0, 'bar0'); + assert.strictEqual(attributes.foo99, 'bar99'); + assert.strictEqual(attributes.foo149, undefined); + }); + }); + + describe('when "attributeValueLengthLimit" option defined', () => { + const { logRecord } = setup({ attributeValueLengthLimit: 5 }); + const { attributes } = logRecord; + + it('should truncate value which length exceeds this limit', () => { + logRecord.setAttribute('attr-with-more-length', 'abcdefgh'); + assert.strictEqual(attributes['attr-with-more-length'], 'abcde'); + }); + + it('should truncate value of arrays which exceeds this limit', () => { + logRecord.setAttribute('attr-array-of-strings', [ + 'abcdefgh', + 'abc', + 'abcde', + '', + ]); + logRecord.setAttribute('attr-array-of-bool', [true, false]); + assert.deepStrictEqual(attributes['attr-array-of-strings'], [ + 'abcde', + 'abc', + 'abcde', + '', + ]); + assert.deepStrictEqual(attributes['attr-array-of-bool'], [ + true, + false, + ]); + }); + + it('should not truncate value which length not exceeds this limit', () => { + logRecord.setAttribute('attr-with-less-length', 'abc'); + assert.strictEqual(attributes['attr-with-less-length'], 'abc'); + }); + + it('should return same value for non-string values', () => { + logRecord.setAttribute('attr-non-string', true); + assert.strictEqual(attributes['attr-non-string'], true); + }); + }); + + describe('when "attributeValueLengthLimit" option is invalid', () => { + const { logRecord } = setup({ attributeValueLengthLimit: -5 }); + const { attributes } = logRecord; + + it('should not truncate any value', () => { + logRecord.setAttribute('attr-not-truncate', 'abcdefgh'); + logRecord.setAttribute('attr-array-of-strings', [ + 'abcdefgh', + 'abc', + 'abcde', + ]); + assert.deepStrictEqual(attributes['attr-not-truncate'], 'abcdefgh'); + assert.deepStrictEqual(attributes['attr-array-of-strings'], [ + 'abcdefgh', + 'abc', + 'abcde', + ]); + }); + }); + }); + }); + + describe('setAttributes', () => { + it('should be able to set multiple attributes', () => { + const { logRecord } = setup(); + logRecord.setAttributes(validAttributes); + logRecord.setAttributes(invalidAttributes as unknown as Attributes); + assert.deepStrictEqual(logRecord.attributes, validAttributes); + }); + }); + + describe('emit', () => { + it('should be emit', () => { + const { logRecord, config } = setup(); + const callSpy = sinon.spy(config.activeProcessor, 'onEmit'); + logRecord.emit(); + assert.ok(callSpy.called); + }); + + it('should have emitted', () => { + const { logRecord } = setup(); + assert.strictEqual(logRecord.emitted, false); + logRecord.emit(); + assert.strictEqual(logRecord.emitted, true); + }); + + it('should be allow emit only once', () => { + const { logRecord, config } = setup(); + const callSpy = sinon.spy(config.activeProcessor, 'onEmit'); + logRecord.emit(); + logRecord.emit(); + assert.ok(callSpy.callCount === 1); + }); + }); + + describe('log record processor', () => { + it('should call onEmit synchronously when log record is emitted', () => { + let emitted = false; + const processor: LogRecordProcessor = { + onEmit: () => { + emitted = true; + }, + forceFlush: () => Promise.resolve(), + shutdown: () => Promise.resolve(), + }; + const provider = new LoggerProvider(); + provider.addLogRecordProcessor(processor); + provider.getLogger('default').emit({ body: 'test' }); + assert.ok(emitted); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts new file mode 100644 index 0000000000..d0155f7d9e --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { Resource } from '@opentelemetry/resources'; + +import { Logger, LogRecord, NoopLogRecordProcessor } from '../../src'; +import { loadDefaultConfig } from '../../src/config'; +import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; + +const setup = () => { + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const logger = new Logger({ + activeProcessor: new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ), + resource: Resource.default(), + logRecordLimits, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }); + return { logger }; +}; + +describe('Logger', () => { + describe('constructor', () => { + it('should create an instance', () => { + const { logger } = setup(); + assert.ok(logger instanceof Logger); + }); + }); + + describe('emit', () => { + it('should emit a logRecord instance', () => { + const { logger } = setup(); + const callSpy = sinon.spy(LogRecord.prototype, 'emit'); + logger.emit({ + body: 'test log body', + }); + assert.ok(callSpy.called); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts new file mode 100644 index 0000000000..9be369a2d7 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts @@ -0,0 +1,213 @@ +/* + * 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 type { LogRecordProcessor, ReadableLogRecord } from '../../src'; +import { + LoggerProvider, + InMemoryLogRecordExporter, + SimpleLogRecordProcessor, +} from './../../src'; +import { loadDefaultConfig } from '../../src/config'; +import { MultiLogRecordProcessor } from './../../src/MultiLogRecordProcessor'; + +class TestProcessor implements LogRecordProcessor { + logRecords: ReadableLogRecord[] = []; + onEmit(span: ReadableLogRecord): void { + this.logRecords.push(span); + } + shutdown(): Promise { + this.logRecords = []; + return Promise.resolve(); + } + forceFlush(): Promise { + return Promise.resolve(); + } +} + +const setup = (processors: LogRecordProcessor[] = []) => { + const { forceFlushTimeoutMillis } = loadDefaultConfig(); + const multiProcessor = new MultiLogRecordProcessor( + processors, + forceFlushTimeoutMillis + ); + return { multiProcessor, forceFlushTimeoutMillis }; +}; + +describe('MultiLogRecordProcessor', () => { + describe('constructor', () => { + it('should create an instance', () => { + assert.ok(setup().multiProcessor instanceof MultiLogRecordProcessor); + }); + }); + + describe('onEmit', () => { + it('should handle empty log record processor', () => { + const { multiProcessor } = setup(); + const provider = new LoggerProvider(); + provider.addLogRecordProcessor(multiProcessor); + const logger = provider.getLogger('default'); + logger.emit({ body: 'one' }); + multiProcessor.shutdown(); + }); + + it('should handle one log record processor', () => { + const processor1 = new TestProcessor(); + const { multiProcessor } = setup([processor1]); + const provider = new LoggerProvider(); + provider.addLogRecordProcessor(multiProcessor); + const logger = provider.getLogger('default'); + assert.strictEqual(processor1.logRecords.length, 0); + + logger.emit({ body: 'one' }); + assert.strictEqual(processor1.logRecords.length, 1); + multiProcessor.shutdown(); + }); + + it('should handle two log record processor', async () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const { multiProcessor } = setup([processor1, processor2]); + const provider = new LoggerProvider(); + provider.addLogRecordProcessor(multiProcessor); + const logger = provider.getLogger('default'); + + assert.strictEqual(processor1.logRecords.length, 0); + assert.strictEqual( + processor1.logRecords.length, + processor2.logRecords.length + ); + + logger.emit({ body: 'one' }); + assert.strictEqual(processor1.logRecords.length, 1); + assert.strictEqual( + processor1.logRecords.length, + processor2.logRecords.length + ); + + await multiProcessor.shutdown(); + assert.strictEqual(processor1.logRecords.length, 0); + assert.strictEqual( + processor1.logRecords.length, + processor2.logRecords.length + ); + }); + }); + + describe('forceFlush', () => { + it('should force log record processors to flush', () => { + let flushed = false; + const processor: LogRecordProcessor = { + forceFlush: () => { + flushed = true; + return Promise.resolve(); + }, + onEmit: () => {}, + shutdown: () => { + return Promise.resolve(); + }, + }; + const { multiProcessor } = setup([processor]); + multiProcessor.forceFlush(); + assert.ok(flushed); + }); + + it('should wait for all log record processors to finish flushing', done => { + let flushed = 0; + const processor1 = new SimpleLogRecordProcessor( + new InMemoryLogRecordExporter() + ); + const processor2 = new SimpleLogRecordProcessor( + new InMemoryLogRecordExporter() + ); + + const spy1 = sinon.stub(processor1, 'forceFlush').callsFake(() => { + flushed++; + return Promise.resolve(); + }); + const spy2 = sinon.stub(processor2, 'forceFlush').callsFake(() => { + flushed++; + return Promise.resolve(); + }); + + const { multiProcessor } = setup([processor1, processor2]); + multiProcessor.forceFlush().then(() => { + sinon.assert.calledOnce(spy1); + sinon.assert.calledOnce(spy2); + assert.strictEqual(flushed, 2); + done(); + }); + }); + + it('should throw error if either time out', async () => { + const processor: LogRecordProcessor = { + forceFlush: () => + new Promise(resolve => { + setTimeout(() => { + resolve(); + }, forceFlushTimeoutMillis + 1000); + }), + onEmit: () => {}, + shutdown: () => { + return Promise.resolve(); + }, + }; + + const clock = sinon.useFakeTimers(); + const { multiProcessor, forceFlushTimeoutMillis } = setup([ + processor, + new TestProcessor(), + ]); + const res = multiProcessor.forceFlush(); + clock.tick(forceFlushTimeoutMillis + 1000); + clock.restore(); + await assert.rejects(res, /Operation timed out/); + }); + }); + + describe('shutdown', () => { + it('should export log records on manual shutdown from two log record processor', async () => { + const processor1 = new TestProcessor(); + const processor2 = new TestProcessor(); + const { multiProcessor } = setup([processor1, processor2]); + const provider = new LoggerProvider(); + provider.addLogRecordProcessor(multiProcessor); + const logger = provider.getLogger('default'); + + assert.strictEqual(processor1.logRecords.length, 0); + assert.strictEqual( + processor1.logRecords.length, + processor2.logRecords.length + ); + + logger.emit({ body: 'one' }); + assert.strictEqual(processor1.logRecords.length, 1); + assert.strictEqual( + processor1.logRecords.length, + processor2.logRecords.length + ); + + await provider.shutdown(); + assert.strictEqual(processor1.logRecords.length, 0); + assert.strictEqual( + processor1.logRecords.length, + processor2.logRecords.length + ); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts new file mode 100644 index 0000000000..dacf4c4b85 --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts @@ -0,0 +1,80 @@ +import { SimpleLogRecordProcessor } from './../../../src/export/SimpleLogRecordProcessor'; +/* + * 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 { SeverityNumber } from '@opentelemetry/api-logs'; + +import { LoggerProvider, ConsoleLogRecordExporter } from './../../../src'; + +describe('ConsoleLogRecordExporter', () => { + let previousConsoleDir: typeof console.dir; + + beforeEach(() => { + previousConsoleDir = console.dir; + console.dir = () => {}; + }); + + afterEach(() => { + console.dir = previousConsoleDir; + }); + + describe('export', () => { + it('should export information about log record', () => { + assert.doesNotThrow(() => { + const consoleExporter = new ConsoleLogRecordExporter(); + const spyConsole = sinon.spy(console, 'dir'); + const spyExport = sinon.spy(consoleExporter, 'export'); + const provider = new LoggerProvider(); + provider.addLogRecordProcessor( + new SimpleLogRecordProcessor(consoleExporter) + ); + + provider.getLogger('default').emit({ + body: 'body1', + severityNumber: SeverityNumber.DEBUG, + severityText: 'DEBUG', + }); + + const logRecords = spyExport.args[0]; + const firstLogRecord = logRecords[0][0]; + const consoleArgs = spyConsole.args[0]; + const consoleLogRecord = consoleArgs[0]; + const keys = Object.keys(consoleLogRecord).sort().join(','); + console.log('11111', keys); + + const expectedKeys = [ + 'attributes', + 'body', + 'severityNumber', + 'severityText', + 'spanId', + 'timestamp', + 'traceFlags', + 'traceId', + ].join(','); + + assert.ok(firstLogRecord.body === 'body1'); + assert.ok(firstLogRecord.severityNumber === SeverityNumber.DEBUG); + assert.ok(firstLogRecord.severityText === 'DEBUG'); + assert.ok(keys === expectedKeys, 'expectedKeys'); + + assert.ok(spyExport.calledOnce); + }); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts new file mode 100644 index 0000000000..b96aaae6ac --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts @@ -0,0 +1,94 @@ +import { SimpleLogRecordProcessor } from './../../../src/export/SimpleLogRecordProcessor'; +/* + * 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 { SeverityNumber } from '@opentelemetry/api-logs'; +import { ExportResult, ExportResultCode } from '@opentelemetry/core'; + +import { LoggerProvider, InMemoryLogRecordExporter } from '../../../src'; + +const setup = () => { + const provider = new LoggerProvider(); + const memoryExporter = new InMemoryLogRecordExporter(); + provider.addLogRecordProcessor(new SimpleLogRecordProcessor(memoryExporter)); + return { provider, memoryExporter }; +}; + +describe('InMemoryLogRecordExporter', () => { + describe('export', () => { + it('should export information about log record', () => { + const { provider, memoryExporter } = setup(); + provider.getLogger('default').emit({ + body: 'body1', + severityNumber: SeverityNumber.DEBUG, + severityText: 'DEBUG', + }); + const logRecords = memoryExporter.getFinishedLogRecords(); + assert.ok(logRecords.length === 1); + + const firstLogRecord = logRecords[0]; + assert.ok(firstLogRecord.body === 'body1'); + assert.ok(firstLogRecord.severityNumber === SeverityNumber.DEBUG); + assert.ok(firstLogRecord.severityText === 'DEBUG'); + }); + + it('should return the success result', () => { + const { memoryExporter } = setup(); + memoryExporter.export([], (result: ExportResult) => { + assert.strictEqual(result.code, ExportResultCode.SUCCESS); + }); + }); + }); + + describe('shutdown', () => { + it('should clear all log records', async () => { + const { provider, memoryExporter } = setup(); + provider.getLogger('default').emit({ + body: 'body1', + severityNumber: SeverityNumber.DEBUG, + severityText: 'DEBUG', + }); + assert.ok(memoryExporter.getFinishedLogRecords().length === 1); + await memoryExporter.shutdown(); + assert.strictEqual(memoryExporter.getFinishedLogRecords().length, 0); + }); + + it('should return failed result after shutdown', () => { + const { memoryExporter } = setup(); + memoryExporter.shutdown(); + + // after shutdown export should fail + memoryExporter.export([], (result: ExportResult) => { + assert.strictEqual(result.code, ExportResultCode.FAILED); + }); + }); + }); + + describe('reset', () => { + it('should clear all log records', () => { + const { provider, memoryExporter } = setup(); + provider.getLogger('default').emit({ + body: 'body1', + severityNumber: SeverityNumber.DEBUG, + severityText: 'DEBUG', + }); + assert.ok(memoryExporter.getFinishedLogRecords().length === 1); + memoryExporter.reset(); + assert.strictEqual(memoryExporter.getFinishedLogRecords().length, 0); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts new file mode 100644 index 0000000000..067064d4fe --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts @@ -0,0 +1,139 @@ +import { LogRecord } from './../../../src/LogRecord'; +/* + * 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 { + ExportResultCode, + loggingErrorHandler, + setGlobalErrorHandler, +} from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; + +import { + InMemoryLogRecordExporter, + LogRecordExporter, + NoopLogRecordProcessor, + SimpleLogRecordProcessor, +} from './../../../src'; +import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; +import { loadDefaultConfig } from '../../../src/config'; + +const setup = (exporter: LogRecordExporter) => { + const processor = new SimpleLogRecordProcessor(exporter); + return { exporter, processor }; +}; + +describe('SimpleLogRecordProcessor', () => { + describe('constructor', () => { + it('should create an instance', () => { + assert.ok( + setup(new InMemoryLogRecordExporter()).processor instanceof + SimpleLogRecordProcessor + ); + }); + }); + + describe('onEmit', () => { + it('should handle onEmit', async () => { + const exporter = new InMemoryLogRecordExporter(); + const { processor } = setup(exporter); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const logRecord = new LogRecord( + { + activeProcessor: new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ), + resource: Resource.default(), + logRecordLimits, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }, + { + body: 'body', + } + ); + processor.onEmit(logRecord); + assert.strictEqual(exporter.getFinishedLogRecords().length, 1); + + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + }); + + it('should call globalErrorHandler when exporting fails', async () => { + const expectedError = new Error('Exporter failed'); + const exporter: LogRecordExporter = { + export: (_, callback) => + setTimeout( + () => + callback({ code: ExportResultCode.FAILED, error: expectedError }), + 0 + ), + shutdown: () => Promise.resolve(), + }; + const { processor } = setup(exporter); + + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + const logRecord = new LogRecord( + { + activeProcessor: new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ), + resource: Resource.default(), + logRecordLimits, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }, + { + body: 'body', + } + ); + const errorHandlerSpy = sinon.spy(); + setGlobalErrorHandler(errorHandlerSpy); + processor.onEmit(logRecord); + await new Promise(resolve => setTimeout(() => resolve(), 0)); + assert.strictEqual(errorHandlerSpy.callCount, 1); + const [[error]] = errorHandlerSpy.args; + assert.deepStrictEqual(error, expectedError); + // reset global error handler + setGlobalErrorHandler(loggingErrorHandler()); + }); + }); + + describe('shutdown', () => { + it('should handle shutdown', async () => { + const shutdownSpy = sinon.spy(); + const exporter: LogRecordExporter = { + export: (_, callback) => callback({ code: ExportResultCode.SUCCESS }), + shutdown: shutdownSpy, + }; + const { processor } = setup(exporter); + await processor.shutdown(); + assert.ok(shutdownSpy.callCount === 1); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/utils.ts b/experimental/packages/sdk-logs/test/common/utils.ts new file mode 100644 index 0000000000..fd801fc30d --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/utils.ts @@ -0,0 +1,33 @@ +/* + * 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 const validAttributes = { + string: 'string', + number: 0, + bool: true, + 'array': ['str1', 'str2'], + 'array': [1, 2], + 'array': [true, false], +}; + +export const invalidAttributes = { + // invalid attribute type object + object: { foo: 'bar' }, + // invalid attribute inhomogeneous array + 'non-homogeneous-array': [0, ''], + // This empty length attribute should not be set + '': 'empty-key', +}; From 8196379585975f923320e8153917ccca4e8f339d Mon Sep 17 00:00:00 2001 From: fugao Date: Sun, 26 Feb 2023 21:20:24 +0800 Subject: [PATCH 15/44] feat(sdk-logs): sdk-logs init --- .../sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts index dacf4c4b85..96fc83a2a3 100644 --- a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts @@ -55,7 +55,6 @@ describe('ConsoleLogRecordExporter', () => { const consoleArgs = spyConsole.args[0]; const consoleLogRecord = consoleArgs[0]; const keys = Object.keys(consoleLogRecord).sort().join(','); - console.log('11111', keys); const expectedKeys = [ 'attributes', From af2ae705974a7e9d5fb41cad3ef897f9df1b7090 Mon Sep 17 00:00:00 2001 From: fugao Date: Mon, 27 Feb 2023 19:40:13 +0800 Subject: [PATCH 16/44] feat(sdk-logs): sdk-logs init --- .../export/BatchLogRecordProcessor.test.ts | 121 ++++++ .../export/BatchLogRecordProcessor.test.ts | 347 ++++++++++++++++++ .../export/ConsoleLogRecordExporter.test.ts | 8 +- .../export/InMemoryLogRecordExporter.test.ts | 7 +- .../export/SimpleLogRecordProcessor.test.ts | 2 +- 5 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts create mode 100644 experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts diff --git a/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts new file mode 100644 index 0000000000..e176f3e7dd --- /dev/null +++ b/experimental/packages/sdk-logs/test/browser/export/BatchLogRecordProcessor.test.ts @@ -0,0 +1,121 @@ +/* + * 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 { LogRecordExporter } from '../../../src'; +import { BatchLogRecordProcessor } from '../../../src/platform/browser/export/BatchLogRecordProcessor'; +import { InMemoryLogRecordExporter } from './../../../src/export/InMemoryLogRecordExporter'; + +const describeDocument = + typeof document === 'object' ? describe : describe.skip; + +describeDocument('BatchLogRecordProcessor - web main context', () => { + let visibilityState: VisibilityState = 'visible'; + let exporter: LogRecordExporter; + let processor: BatchLogRecordProcessor; + let forceFlushSpy: sinon.SinonStub; + let visibilityChangeEvent: Event; + let pageHideEvent: Event; + + beforeEach(() => { + sinon.replaceGetter(document, 'visibilityState', () => visibilityState); + visibilityState = 'visible'; + exporter = new InMemoryLogRecordExporter(); + processor = new BatchLogRecordProcessor(exporter, {}); + forceFlushSpy = sinon.stub(processor, 'forceFlush'); + visibilityChangeEvent = new Event('visibilitychange'); + pageHideEvent = new Event('pagehide'); + }); + + afterEach(async () => { + sinon.restore(); + }); + + describe('when document becomes hidden', () => { + const testDocumentHide = (hideDocument: () => void) => { + it('should force flush log records', () => { + assert.strictEqual(forceFlushSpy.callCount, 0); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 1); + }); + + describe('AND shutdown has been called', () => { + it('should NOT force flush log records', async () => { + assert.strictEqual(forceFlushSpy.callCount, 0); + await processor.shutdown(); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 0); + }); + }); + + describe('AND disableAutoFlushOnDocumentHide configuration option', () => { + it('set to false should force flush log records', () => { + processor = new BatchLogRecordProcessor(exporter, { + disableAutoFlushOnDocumentHide: false, + }); + forceFlushSpy = sinon.stub(processor, 'forceFlush'); + assert.strictEqual(forceFlushSpy.callCount, 0); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 1); + }); + + it('set to true should NOT force flush log records', () => { + processor = new BatchLogRecordProcessor(exporter, { + disableAutoFlushOnDocumentHide: true, + }); + forceFlushSpy = sinon.stub(processor, 'forceFlush'); + assert.strictEqual(forceFlushSpy.callCount, 0); + hideDocument(); + assert.strictEqual(forceFlushSpy.callCount, 0); + }); + }); + }; + + describe('by the visibilitychange event', () => { + testDocumentHide(() => { + visibilityState = 'hidden'; + document.dispatchEvent(visibilityChangeEvent); + }); + }); + + describe('by the pagehide event', () => { + testDocumentHide(() => { + document.dispatchEvent(pageHideEvent); + }); + }); + }); + + describe('when document becomes visible', () => { + it('should NOT force flush log records', () => { + assert.strictEqual(forceFlushSpy.callCount, 0); + document.dispatchEvent(visibilityChangeEvent); + assert.strictEqual(forceFlushSpy.callCount, 0); + }); + }); +}); + +describe('BatchLogRecordProcessor', () => { + it('without exception', async () => { + const exporter = new InMemoryLogRecordExporter(); + const logRecordProcessor = new BatchLogRecordProcessor(exporter); + assert.ok(logRecordProcessor instanceof BatchLogRecordProcessor); + + await logRecordProcessor.forceFlush(); + await logRecordProcessor.shutdown(); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts new file mode 100644 index 0000000000..cac127df6a --- /dev/null +++ b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts @@ -0,0 +1,347 @@ +/* + * 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 { Resource } from '@opentelemetry/resources'; +import { + ExportResultCode, + getEnv, + loggingErrorHandler, + setGlobalErrorHandler, +} from '@opentelemetry/core'; + +import { + BufferConfig, + LogRecordLimits, + NoopLogRecordProcessor, + BatchLogRecordProcessorBase, + LogRecord, + InMemoryLogRecordExporter, +} from '../../../src'; +import { loadDefaultConfig } from '../../../src/config'; +import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; + +class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { + onInit() {} + onShutdown() {} +} + +const createLogRecord = (limits?: LogRecordLimits): LogRecord => { + const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); + + const logRecord = new LogRecord( + { + activeProcessor: new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ), + resource: Resource.default(), + logRecordLimits: { + attributeValueLengthLimit: + limits?.attributeValueLengthLimit ?? + logRecordLimits.attributeValueLengthLimit, + attributeCountLimit: + limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, + }, + instrumentationScope: { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, + }, + {} + ); + return logRecord; +}; + +describe('BatchLogRecordProcessorBase', () => { + const defaultBufferConfig = { + maxExportBatchSize: 5, + scheduledDelayMillis: 2500, + }; + let exporter: InMemoryLogRecordExporter; + + beforeEach(() => { + exporter = new InMemoryLogRecordExporter(); + }); + + afterEach(() => { + exporter.reset(); + sinon.restore(); + }); + + describe('constructor', () => { + it('should create a BatchLogRecordProcessor instance', () => { + const processor = new BatchLogRecordProcessor(exporter); + assert.ok(processor instanceof BatchLogRecordProcessor); + processor.shutdown(); + }); + + it('should create a BatchLogRecordProcessor instance with config', () => { + const bufferConfig = { + maxExportBatchSize: 5, + scheduledDelayMillis: 2500, + exportTimeoutMillis: 2000, + maxQueueSize: 200, + }; + const processor = new BatchLogRecordProcessor(exporter, bufferConfig); + assert.ok(processor instanceof BatchLogRecordProcessor); + assert.strictEqual( + processor['_maxExportBatchSize'], + bufferConfig.maxExportBatchSize + ); + assert.strictEqual(processor['_maxQueueSize'], bufferConfig.maxQueueSize); + assert.strictEqual( + processor['_scheduledDelayMillis'], + bufferConfig.scheduledDelayMillis + ); + assert.strictEqual( + processor['_exportTimeoutMillis'], + bufferConfig.exportTimeoutMillis + ); + processor.shutdown(); + }); + + it('should create a BatchLogRecordProcessor instance with empty config', () => { + const processor = new BatchLogRecordProcessor(exporter); + + const { + OTEL_BSP_MAX_EXPORT_BATCH_SIZE, + OTEL_BSP_MAX_QUEUE_SIZE, + OTEL_BSP_SCHEDULE_DELAY, + OTEL_BSP_EXPORT_TIMEOUT, + } = getEnv(); + assert.ok(processor instanceof BatchLogRecordProcessor); + assert.strictEqual( + processor['_maxExportBatchSize'], + OTEL_BSP_MAX_EXPORT_BATCH_SIZE + ); + assert.strictEqual(processor['_maxQueueSize'], OTEL_BSP_MAX_QUEUE_SIZE); + assert.strictEqual( + processor['_scheduledDelayMillis'], + OTEL_BSP_SCHEDULE_DELAY + ); + assert.strictEqual( + processor['_exportTimeoutMillis'], + OTEL_BSP_EXPORT_TIMEOUT + ); + processor.shutdown(); + }); + + it('maxExportBatchSize must be smaller or equal to maxQueueSize', () => { + const bufferConfig = { + maxExportBatchSize: 200, + maxQueueSize: 100, + }; + const processor = new BatchLogRecordProcessor(exporter, bufferConfig); + assert.strictEqual( + processor['_maxExportBatchSize'], + processor['_maxQueueSize'] + ); + }); + }); + + describe('onEmit', () => { + it('should export the log records with buffer size reached', done => { + const clock = sinon.useFakeTimers(); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + processor.onEmit(logRecord); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + } + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + setTimeout(async () => { + assert.strictEqual( + exporter.getFinishedLogRecords().length, + defaultBufferConfig.maxExportBatchSize + ); + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + done(); + }, defaultBufferConfig.scheduledDelayMillis + 1000); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + clock.restore(); + }); + + it('should force flush when timeout exceeded', done => { + const clock = sinon.useFakeTimers(); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + } + setTimeout(() => { + assert.strictEqual( + exporter.getFinishedLogRecords().length, + defaultBufferConfig.maxExportBatchSize + ); + done(); + }, defaultBufferConfig.scheduledDelayMillis + 1000); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + clock.restore(); + }); + + it('should not export empty log record lists', done => { + const spy = sinon.spy(exporter, 'export'); + const clock = sinon.useFakeTimers(); + new BatchLogRecordProcessor(exporter, defaultBufferConfig); + setTimeout(() => { + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + sinon.assert.notCalled(spy); + done(); + }, defaultBufferConfig.scheduledDelayMillis + 1000); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + + clock.restore(); + }); + + it('should export each log record exactly once with buffer size reached multiple times', done => { + const originalTimeout = setTimeout; + const clock = sinon.useFakeTimers(); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + const totalLogRecords = defaultBufferConfig.maxExportBatchSize * 2; + for (let i = 0; i < totalLogRecords; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + } + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); + originalTimeout(() => { + clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); + originalTimeout(async () => { + clock.tick(defaultBufferConfig.scheduledDelayMillis + 10); + clock.restore(); + assert.strictEqual( + exporter.getFinishedLogRecords().length, + totalLogRecords + 1 + ); + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + done(); + }); + }); + }); + + it('should call globalErrorHandler when exporting fails', done => { + const clock = sinon.useFakeTimers(); + const expectedError = new Error('Exporter failed'); + sinon.stub(exporter, 'export').callsFake((_, callback) => { + setTimeout(() => { + callback({ code: ExportResultCode.FAILED, error: expectedError }); + }, 0); + }); + const errorHandlerSpy = sinon.spy(); + setGlobalErrorHandler(errorHandlerSpy); + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + } + clock.tick(defaultBufferConfig.scheduledDelayMillis + 1000); + clock.restore(); + setTimeout(() => { + assert.strictEqual(errorHandlerSpy.callCount, 1); + const [[error]] = errorHandlerSpy.args; + assert.deepStrictEqual(error, expectedError); + // reset global error handler + setGlobalErrorHandler(loggingErrorHandler()); + done(); + }); + }); + + it('should drop logRecords when there are more logRecords then "maxQueueSize"', () => { + const maxQueueSize = 6; + const processor = new BatchLogRecordProcessor(exporter, { maxQueueSize }); + const logRecord = createLogRecord(); + for (let i = 0; i < maxQueueSize + 10; i++) { + processor.onEmit(logRecord); + } + assert.strictEqual(processor['_finishedLogRecords'].length, 6); + }); + }); + + describe('forceFlush', () => { + it('should force flush on demand', () => { + const processor = new BatchLogRecordProcessor( + exporter, + defaultBufferConfig + ); + for (let i = 0; i < defaultBufferConfig.maxExportBatchSize; i++) { + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + } + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + processor.forceFlush(); + assert.strictEqual( + exporter.getFinishedLogRecords().length, + defaultBufferConfig.maxExportBatchSize + ); + }); + + it('should call an async callback when flushing is complete', async () => { + const processor = new BatchLogRecordProcessor(exporter); + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + await processor.forceFlush(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 1); + }); + }); + + describe('shutdown', () => { + it('should call onShutdown', async () => { + const processor = new BatchLogRecordProcessor(exporter); + const onShutdownSpy = sinon.stub(processor, 'onShutdown'); + assert.strictEqual(onShutdownSpy.callCount, 0); + await processor.shutdown(); + assert.strictEqual(onShutdownSpy.callCount, 1); + }); + + it('should call an async callback when shutdown is complete', async () => { + let exportedLogRecords = 0; + sinon.stub(exporter, 'export').callsFake((logRecords, callback) => { + setTimeout(() => { + exportedLogRecords = exportedLogRecords + logRecords.length; + callback({ code: ExportResultCode.SUCCESS }); + }, 0); + }); + const processor = new BatchLogRecordProcessor(exporter); + const logRecord = createLogRecord(); + processor.onEmit(logRecord); + await processor.shutdown(); + assert.strictEqual(exporter.getFinishedLogRecords().length, 0); + assert.strictEqual(exportedLogRecords, 1); + }); + }); +}); diff --git a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts index 96fc83a2a3..bd28e8e12e 100644 --- a/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/ConsoleLogRecordExporter.test.ts @@ -1,4 +1,3 @@ -import { SimpleLogRecordProcessor } from './../../../src/export/SimpleLogRecordProcessor'; /* * Copyright The OpenTelemetry Authors * @@ -19,8 +18,13 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { SeverityNumber } from '@opentelemetry/api-logs'; -import { LoggerProvider, ConsoleLogRecordExporter } from './../../../src'; +import { + LoggerProvider, + ConsoleLogRecordExporter, + SimpleLogRecordProcessor, +} from './../../../src'; +/* eslint-disable no-console */ describe('ConsoleLogRecordExporter', () => { let previousConsoleDir: typeof console.dir; diff --git a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts index b96aaae6ac..410ec5a23a 100644 --- a/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/InMemoryLogRecordExporter.test.ts @@ -1,4 +1,3 @@ -import { SimpleLogRecordProcessor } from './../../../src/export/SimpleLogRecordProcessor'; /* * Copyright The OpenTelemetry Authors * @@ -19,7 +18,11 @@ import * as assert from 'assert'; import { SeverityNumber } from '@opentelemetry/api-logs'; import { ExportResult, ExportResultCode } from '@opentelemetry/core'; -import { LoggerProvider, InMemoryLogRecordExporter } from '../../../src'; +import { + LoggerProvider, + InMemoryLogRecordExporter, + SimpleLogRecordProcessor, +} from '../../../src'; const setup = () => { const provider = new LoggerProvider(); diff --git a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts index 067064d4fe..c5f72ab8a2 100644 --- a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts @@ -1,4 +1,3 @@ -import { LogRecord } from './../../../src/LogRecord'; /* * Copyright The OpenTelemetry Authors * @@ -29,6 +28,7 @@ import { LogRecordExporter, NoopLogRecordProcessor, SimpleLogRecordProcessor, + LogRecord, } from './../../../src'; import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; import { loadDefaultConfig } from '../../../src/config'; From 681d937a671435107cb19a639444eddd990ce198 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 28 Feb 2023 15:03:33 +0800 Subject: [PATCH 17/44] feat(sdk-logs): sdk-logs init --- ...atchLogRecordProcessor.ts => BatchLogRecordProcessorBase.ts} | 2 +- experimental/packages/sdk-logs/src/index.ts | 1 - .../src/platform/browser/export/BatchLogRecordProcessor.ts | 2 +- .../src/platform/node/export/BatchLogRecordProcessor.ts | 2 +- .../sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) rename experimental/packages/sdk-logs/src/export/{BatchLogRecordProcessor.ts => BatchLogRecordProcessorBase.ts} (98%) diff --git a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts similarity index 98% rename from experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts rename to experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts index 95aacdb58b..2e45e70d6f 100644 --- a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts @@ -28,7 +28,7 @@ import { import type { BufferConfig } from '../types'; import type { ReadableLogRecord } from './ReadableLogRecord'; import type { LogRecordExporter } from './LogRecordExporter'; -import type { LogRecordProcessor } from './../LogRecordProcessor'; +import type { LogRecordProcessor } from '../LogRecordProcessor'; export abstract class BatchLogRecordProcessorBase implements LogRecordProcessor diff --git a/experimental/packages/sdk-logs/src/index.ts b/experimental/packages/sdk-logs/src/index.ts index a119b0a362..11dcb82f20 100644 --- a/experimental/packages/sdk-logs/src/index.ts +++ b/experimental/packages/sdk-logs/src/index.ts @@ -21,7 +21,6 @@ export * from './LogRecord'; export * from './LogRecordProcessor'; export * from './export/ReadableLogRecord'; export * from './export/NoopLogRecordProcessor'; -export * from './export/BatchLogRecordProcessor'; export * from './export/ConsoleLogRecordExporter'; export * from './export/LogRecordExporter'; export * from './export/SimpleLogRecordProcessor'; diff --git a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts index 8437834375..f201b59b48 100644 --- a/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/platform/browser/export/BatchLogRecordProcessor.ts @@ -16,7 +16,7 @@ import type { LogRecordExporter } from './../../../export/LogRecordExporter'; import type { BatchLogRecordProcessorBrowserConfig } from '../../../types'; -import { BatchLogRecordProcessorBase } from '../../../export/BatchLogRecordProcessor'; +import { BatchLogRecordProcessorBase } from '../../../export/BatchLogRecordProcessorBase'; export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { private _visibilityChangeListener?: () => void; diff --git a/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts index 70bb073f5c..82350612c6 100644 --- a/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/platform/node/export/BatchLogRecordProcessor.ts @@ -15,7 +15,7 @@ */ import type { BufferConfig } from '../../../types'; -import { BatchLogRecordProcessorBase } from '../../../export/BatchLogRecordProcessor'; +import { BatchLogRecordProcessorBase } from '../../../export/BatchLogRecordProcessorBase'; export class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { protected onShutdown(): void {} diff --git a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts index cac127df6a..855c52770c 100644 --- a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts @@ -28,12 +28,12 @@ import { BufferConfig, LogRecordLimits, NoopLogRecordProcessor, - BatchLogRecordProcessorBase, LogRecord, InMemoryLogRecordExporter, } from '../../../src'; import { loadDefaultConfig } from '../../../src/config'; import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; +import { BatchLogRecordProcessorBase } from '../../../src/export/BatchLogRecordProcessorBase'; class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { onInit() {} From b06e7f3d217cfaa17e09112522dffac408e09f63 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 28 Feb 2023 15:37:45 +0800 Subject: [PATCH 18/44] feat(sdk-logs): sdk-logs init --- experimental/packages/sdk-logs/README.md | 11 ++--------- experimental/packages/sdk-logs/src/LogRecord.ts | 16 +++++++--------- .../packages/sdk-logs/src/LogRecordProcessor.ts | 6 +++--- .../sdk-logs/src/MultiLogRecordProcessor.ts | 4 ++-- .../src/export/BatchLogRecordProcessorBase.ts | 10 +++++----- .../src/export/SimpleLogRecordProcessor.ts | 4 ++-- 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/experimental/packages/sdk-logs/README.md b/experimental/packages/sdk-logs/README.md index 1ef5ca4692..daa86e1e2d 100644 --- a/experimental/packages/sdk-logs/README.md +++ b/experimental/packages/sdk-logs/README.md @@ -44,15 +44,8 @@ const logger = loggerProvider.getLogger('default'); logsAPI.logs.setGlobalLoggerProvider(loggerProvider); const logger = logsAPI.logs.getLogger('default'); -// logging an event in an instrumentation library -logger.emitEvent({ - name: 'event-name', - body: 'this is a log event body', - attributes: { 'log.type': 'LogEvent' }, -}); - -// logging an event in a log appender -logger.emitLogRecord({ +// emit a log record +logger.emit({ severityNumber: SeverityNumber.INFO, severityText: 'INFO', body: 'this is a log record body', diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 48cd33d2d6..267d503065 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -70,7 +70,8 @@ export class LogRecord implements ReadableLogRecord { } public emit(): void { - if (this._isLogRecordEmitted()) { + if (this._isEmitted) { + api.diag.warn('Can not emit an emitted LogRecord'); return; } this._isEmitted = true; @@ -78,7 +79,11 @@ export class LogRecord implements ReadableLogRecord { } public setAttribute(key: string, value?: AttributeValue) { - if (value === null || this._isLogRecordEmitted()) { + if (value === null) { + return; + } + if (this._isEmitted) { + api.diag.warn('Can not setAttribute on emitted LogRecord'); return; } if (key.length === 0) { @@ -109,13 +114,6 @@ export class LogRecord implements ReadableLogRecord { return this._isEmitted; } - private _isLogRecordEmitted(): boolean { - if (this._isEmitted) { - api.diag.warn('Can not execute the operation on emitted LogRecord'); - } - return this._isEmitted; - } - private _truncateToSize(value: AttributeValue): AttributeValue { const limit = this._config.logRecordLimits.attributeValueLengthLimit || 0; // Check limit diff --git a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts index 6b81e27a63..9cd6864882 100644 --- a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ReadableLogRecord } from './export/ReadableLogRecord'; +import { LogRecord } from './LogRecord'; export interface LogRecordProcessor { /** @@ -23,10 +23,10 @@ export interface LogRecordProcessor { forceFlush(): Promise; /** - * Called when a {@link ReadableLogRecord} is emit + * Called when a {@link LogRecord} is emit * @param logRecord the ReadableLogRecord that just emitted. */ - onEmit(logRecord: ReadableLogRecord): void; + onEmit(logRecord: LogRecord): void; /** * Shuts down the processor. Called when SDK is shut down. This is an diff --git a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts index 9b09fff731..c4e5031261 100644 --- a/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/MultiLogRecordProcessor.ts @@ -17,7 +17,7 @@ import { callWithTimeout } from '@opentelemetry/core'; import type { LogRecordProcessor } from './LogRecordProcessor'; -import type { ReadableLogRecord } from './export/ReadableLogRecord'; +import type { LogRecord } from './LogRecord'; /** * Implementation of the {@link LogRecordProcessor} that simply forwards all @@ -38,7 +38,7 @@ export class MultiLogRecordProcessor implements LogRecordProcessor { ); } - public onEmit(logRecord: ReadableLogRecord): void { + public onEmit(logRecord: LogRecord): void { this.processors.forEach(processors => processors.onEmit(logRecord)); } diff --git a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts index 2e45e70d6f..f8a1331f09 100644 --- a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts +++ b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts @@ -26,7 +26,7 @@ import { } from '@opentelemetry/core'; import type { BufferConfig } from '../types'; -import type { ReadableLogRecord } from './ReadableLogRecord'; +import type { LogRecord } from '../LogRecord'; import type { LogRecordExporter } from './LogRecordExporter'; import type { LogRecordProcessor } from '../LogRecordProcessor'; @@ -38,7 +38,7 @@ export abstract class BatchLogRecordProcessorBase private readonly _scheduledDelayMillis: number; private readonly _exportTimeoutMillis: number; - private _finishedLogRecords: ReadableLogRecord[] = []; + private _finishedLogRecords: LogRecord[] = []; private _timer: NodeJS.Timeout | undefined; private _shutdownOnce: BindOnceFuture; @@ -62,7 +62,7 @@ export abstract class BatchLogRecordProcessorBase } } - public onEmit(logRecord: ReadableLogRecord): void { + public onEmit(logRecord: LogRecord): void { if (this._shutdownOnce.isCalled) { return; } @@ -87,7 +87,7 @@ export abstract class BatchLogRecordProcessorBase } /** Add a LogRecord in the buffer. */ - private _addToBuffer(logRecord: ReadableLogRecord) { + private _addToBuffer(logRecord: LogRecord) { if (this._finishedLogRecords.length >= this._maxQueueSize) { return; } @@ -160,7 +160,7 @@ export abstract class BatchLogRecordProcessorBase } } - private _export(logRecords: ReadableLogRecord[]): Promise { + private _export(logRecords: LogRecord[]): Promise { return new Promise((resolve, reject) => { this._exporter.export(logRecords, (res: ExportResult) => { if (res.code !== ExportResultCode.SUCCESS) { diff --git a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts index cfe8aea651..467cf6e322 100644 --- a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts @@ -23,7 +23,7 @@ import { import type { LogRecordExporter } from './LogRecordExporter'; import type { LogRecordProcessor } from '../LogRecordProcessor'; -import type { ReadableLogRecord } from './ReadableLogRecord'; +import type { LogRecord } from './../LogRecord'; export class SimpleLogRecordProcessor implements LogRecordProcessor { private _shutdownOnce: BindOnceFuture; @@ -32,7 +32,7 @@ export class SimpleLogRecordProcessor implements LogRecordProcessor { this._shutdownOnce = new BindOnceFuture(this._shutdown, this); } - public onEmit(logRecord: ReadableLogRecord): void { + public onEmit(logRecord: LogRecord): void { if (this._shutdownOnce.isCalled) { return; } From 6448b43c5aff33b636298160b420c3468943ee7a Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 28 Feb 2023 20:57:28 +0800 Subject: [PATCH 19/44] feat(sdk-logs): sdk-logs init --- .../packages/sdk-logs/src/LogRecord.ts | 39 +++----- experimental/packages/sdk-logs/src/Logger.ts | 27 +++++- .../packages/sdk-logs/src/LoggerProvider.ts | 37 ++++---- experimental/packages/sdk-logs/src/config.ts | 59 +++++++++++- .../src/export/ConsoleLogRecordExporter.ts | 2 +- .../src/export/SimpleLogRecordProcessor.ts | 2 +- experimental/packages/sdk-logs/src/types.ts | 17 +--- .../sdk-logs/test/common/LogRecord.test.ts | 89 ++++++------------- .../sdk-logs/test/common/Logger.test.ts | 20 ++--- .../test/common/LoggerProvider.test.ts | 10 +-- .../common/MultiLogRecordProcessor.test.ts | 4 +- .../export/BatchLogRecordProcessor.test.ts | 36 +++----- .../export/SimpleLogRecordProcessor.test.ts | 56 +++++------- 13 files changed, 185 insertions(+), 213 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 267d503065..3f07aeffb1 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -23,10 +23,11 @@ import { timeInputToHrTime, isAttributeValue, } from '@opentelemetry/core'; - import type { IResource } from '@opentelemetry/resources'; + import type { ReadableLogRecord } from './export/ReadableLogRecord'; -import type { LoggerConfig } from './types'; +import type { LogRecordLimits } from './types'; +import { Logger } from './Logger'; export class LogRecord implements ReadableLogRecord { readonly time: api.HrTime; @@ -39,13 +40,9 @@ export class LogRecord implements ReadableLogRecord { readonly resource: IResource; readonly instrumentationScope: InstrumentationScope; readonly attributes: Attributes = {}; + private readonly _logRecordLimits: LogRecordLimits; - private _isEmitted = false; - - constructor( - private readonly _config: LoggerConfig, - logRecord: logsAPI.LogRecord - ) { + constructor(logger: Logger, logRecord: logsAPI.LogRecord) { const { timestamp = hrTime(), severityNumber, @@ -64,28 +61,16 @@ export class LogRecord implements ReadableLogRecord { this.severityNumber = severityNumber; this.severityText = severityText; this.body = body; - this.resource = this._config.resource; - this.instrumentationScope = this._config.instrumentationScope; + this.resource = logger.resource; + this.instrumentationScope = logger.instrumentationScope; + this._logRecordLimits = logger.getLogRecordLimits(); this.setAttributes(attributes); } - public emit(): void { - if (this._isEmitted) { - api.diag.warn('Can not emit an emitted LogRecord'); - return; - } - this._isEmitted = true; - this._config.activeProcessor.onEmit(this); - } - public setAttribute(key: string, value?: AttributeValue) { if (value === null) { return; } - if (this._isEmitted) { - api.diag.warn('Can not setAttribute on emitted LogRecord'); - return; - } if (key.length === 0) { api.diag.warn(`Invalid attribute key: ${key}`); return; @@ -96,7 +81,7 @@ export class LogRecord implements ReadableLogRecord { } if ( Object.keys(this.attributes).length >= - this._config.logRecordLimits.attributeCountLimit! && + this._logRecordLimits.attributeCountLimit! && !Object.prototype.hasOwnProperty.call(this.attributes, key) ) { return; @@ -110,12 +95,8 @@ export class LogRecord implements ReadableLogRecord { } } - get emitted(): boolean { - return this._isEmitted; - } - private _truncateToSize(value: AttributeValue): AttributeValue { - const limit = this._config.logRecordLimits.attributeValueLengthLimit || 0; + const limit = this._logRecordLimits.attributeValueLengthLimit || 0; // Check limit if (limit <= 0) { // Negative values are invalid, so do not truncate diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index 8e7d1e7ed0..cacefbe426 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -15,18 +15,39 @@ */ import type * as logsAPI from '@opentelemetry/api-logs'; +import type { IResource } from '@opentelemetry/resources'; +import type { InstrumentationScope } from '@opentelemetry/core'; import type { LoggerConfig, LogRecordLimits } from './types'; import { LogRecord } from './LogRecord'; +import { LoggerProvider } from './LoggerProvider'; +import { mergeConfig } from './config'; +import { LogRecordProcessor } from './LogRecordProcessor'; export class Logger implements logsAPI.Logger { - constructor(private readonly _config: LoggerConfig) {} + public readonly resource: IResource; + private readonly _logRecordLimits: LogRecordLimits; + + constructor( + public readonly instrumentationScope: InstrumentationScope, + config: LoggerConfig, + private _loggerProvider: LoggerProvider + ) { + const localConfig = mergeConfig(config); + this.resource = _loggerProvider.resource; + this._logRecordLimits = localConfig.logRecordLimits!; + } public emit(logRecord: logsAPI.LogRecord): void { - new LogRecord(this._config, logRecord).emit(); + const logRecordInstance = new LogRecord(this, logRecord); + this.getActiveLogRecordProcessor().onEmit(logRecordInstance); } public getLogRecordLimits(): LogRecordLimits { - return this._config.logRecordLimits; + return this._logRecordLimits; + } + + public getActiveLogRecordProcessor(): LogRecordProcessor { + return this._loggerProvider.getActiveLogRecordProcessor(); } } diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 0ab2040e17..abeec3e3de 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -18,11 +18,11 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import { IResource, Resource } from '@opentelemetry/resources'; import { getEnv, merge } from '@opentelemetry/core'; -import type { LoggerProviderConfig, LogRecordLimits } from './types'; +import type { LoggerConfig } from './types'; import type { LogRecordProcessor } from './LogRecordProcessor'; import type { LogRecordExporter } from './export/LogRecordExporter'; import { Logger } from './Logger'; -import { loadDefaultConfig } from './config'; +import { loadDefaultConfig, reconfigureLimits } from './config'; import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; import { BatchLogRecordProcessor } from './platform/node/export/BatchLogRecordProcessor'; import { NoopLogRecordProcessor } from './export/NoopLogRecordProcessor'; @@ -38,20 +38,22 @@ export class LoggerProvider implements logsAPI.LoggerProvider { public readonly resource: IResource; private readonly _loggers: Map = new Map(); - private readonly _logRecordLimits: LogRecordLimits; private _activeProcessor: MultiLogRecordProcessor; private readonly _registeredLogRecordProcessors: LogRecordProcessor[] = []; - private readonly _forceFlushTimeoutMillis; + private readonly _config: LoggerConfig; - constructor(config: LoggerProviderConfig = {}) { - const { resource, logRecordLimits, forceFlushTimeoutMillis } = merge( - {}, - loadDefaultConfig(), - config - ); - this.resource = Resource.default().merge(resource ?? Resource.empty()); - this._logRecordLimits = logRecordLimits; - this._forceFlushTimeoutMillis = forceFlushTimeoutMillis; + constructor(config: LoggerConfig = {}) { + const { + resource = Resource.empty(), + logRecordLimits, + forceFlushTimeoutMillis, + } = merge({}, loadDefaultConfig(), reconfigureLimits(config)); + this.resource = Resource.default().merge(resource); + this._config = { + logRecordLimits, + resource: this.resource, + forceFlushTimeoutMillis, + }; const defaultExporter = this._buildExporterFromEnv(); if (defaultExporter !== undefined) { @@ -80,12 +82,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { if (!this._loggers.has(key)) { this._loggers.set( key, - new Logger({ - resource: this.resource, - logRecordLimits: this._logRecordLimits, - activeProcessor: this._activeProcessor, - instrumentationScope: { name, version, schemaUrl }, - }) + new Logger({ name, version, schemaUrl }, this._config, this) ); } return this._loggers.get(key)!; @@ -111,7 +108,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { this._registeredLogRecordProcessors.push(processor); this._activeProcessor = new MultiLogRecordProcessor( this._registeredLogRecordProcessors, - this._forceFlushTimeoutMillis + this._config.forceFlushTimeoutMillis! ); } diff --git a/experimental/packages/sdk-logs/src/config.ts b/experimental/packages/sdk-logs/src/config.ts index 40f60e25fd..2b2e12c3e3 100644 --- a/experimental/packages/sdk-logs/src/config.ts +++ b/experimental/packages/sdk-logs/src/config.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { getEnv } from '@opentelemetry/core'; +import { + DEFAULT_ATTRIBUTE_COUNT_LIMIT, + DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, + getEnv, + getEnvWithoutDefaults, +} from '@opentelemetry/core'; +import { LoggerConfig, LogRecordLimits } from './types'; export function loadDefaultConfig() { return { @@ -27,4 +33,55 @@ export function loadDefaultConfig() { }; } +/** + * When general limits are provided and model specific limits are not, + * configures the model specific limits by using the values from the general ones. + * @param userConfig User provided tracer configuration + */ +export function reconfigureLimits(userConfig: LoggerConfig): LoggerConfig { + const logRecordLimits = Object.assign({}, userConfig.logRecordLimits); + + const parsedEnvConfig = getEnvWithoutDefaults(); + + /** + * Reassign log record attribute count limit to use first non null value defined by user or use default value + */ + logRecordLimits.attributeCountLimit = + userConfig.logRecordLimits?.attributeCountLimit ?? + parsedEnvConfig.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT ?? + parsedEnvConfig.OTEL_ATTRIBUTE_COUNT_LIMIT ?? + DEFAULT_ATTRIBUTE_COUNT_LIMIT; + + /** + * Reassign log record attribute value length limit to use first non null value defined by user or use default value + */ + logRecordLimits.attributeValueLengthLimit = + userConfig.logRecordLimits?.attributeValueLengthLimit ?? + parsedEnvConfig.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT ?? + parsedEnvConfig.OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT ?? + DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT; + + return Object.assign({}, userConfig, { logRecordLimits }); +} + +/** + * Function to merge Default configuration (as specified in './config') with + * user provided configurations. + */ +export function mergeConfig(userConfig: LoggerConfig): LoggerConfig & { + logRecordLimits: LogRecordLimits; +} { + const DEFAULT_CONFIG = loadDefaultConfig(); + + const target = Object.assign({}, DEFAULT_CONFIG, userConfig); + + target.logRecordLimits = Object.assign( + {}, + DEFAULT_CONFIG.logRecordLimits, + userConfig.logRecordLimits || {} + ); + + return target; +} + export const DEFAULT_EVENT_DOMAIN = 'default'; diff --git a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts index bff34a9ec5..cfdbc23a6a 100644 --- a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts +++ b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts @@ -48,7 +48,7 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { /** * converts logRecord info into more readable format - * @param span + * @param logRecord */ private _exportInfo(logRecord: ReadableLogRecord) { return { diff --git a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts index 467cf6e322..b516d55fc7 100644 --- a/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/export/SimpleLogRecordProcessor.ts @@ -51,7 +51,7 @@ export class SimpleLogRecordProcessor implements LogRecordProcessor { } public forceFlush(): Promise { - // do nothing as all spans are being exported without waiting + // do nothing as all log records are being exported without waiting return Promise.resolve(); } diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index dcff384d35..34246d577d 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -15,16 +15,14 @@ */ import type { IResource } from '@opentelemetry/resources'; -import type { InstrumentationScope } from '@opentelemetry/core'; -import type { LogRecordProcessor } from './LogRecordProcessor'; - -export interface LoggerProviderConfig { - /** Resource associated with trace telemetry */ - resource?: IResource; +export interface LoggerConfig { /** Log Record Limits*/ logRecordLimits?: LogRecordLimits; + /** Resource associated with trace telemetry */ + resource?: IResource; + /** * How long the forceFlush can run before it is cancelled. * The default value is 30000ms @@ -40,13 +38,6 @@ export interface LogRecordLimits { attributeCountLimit?: number; } -export interface LoggerConfig { - activeProcessor: LogRecordProcessor; - resource: IResource; - logRecordLimits: LogRecordLimits; - instrumentationScope: InstrumentationScope; -} - /** Interface configuration for a buffer. */ export interface BufferConfig { /** The maximum batch size of every export. It must be smaller or equal to diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts index 7e3b58d848..a27fd42f37 100644 --- a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts +++ b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts @@ -18,46 +18,37 @@ import type { Attributes, AttributeValue } from '@opentelemetry/api'; import * as logsAPI from '@opentelemetry/api-logs'; import type { HrTime } from '@opentelemetry/api'; import * as assert from 'assert'; -import * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; import { hrTimeToMilliseconds, timeInputToHrTime } from '@opentelemetry/core'; +import { Resource } from '@opentelemetry/resources'; import { LogRecordLimits, LogRecordProcessor, - NoopLogRecordProcessor, LogRecord, + Logger, LoggerProvider, } from './../../src'; -import { loadDefaultConfig } from '../../src/config'; -import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; import { invalidAttributes, validAttributes } from './utils'; const performanceTimeOrigin: HrTime = [1, 1]; const setup = (limits?: LogRecordLimits, data?: logsAPI.LogRecord) => { - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const config = { - activeProcessor: new MultiLogRecordProcessor( - [new NoopLogRecordProcessor()], - forceFlushTimeoutMillis - ), - resource: Resource.default(), - logRecordLimits: { - attributeValueLengthLimit: - limits?.attributeValueLengthLimit ?? - logRecordLimits.attributeValueLengthLimit, - attributeCountLimit: - limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, - }, - instrumentationScope: { - name: 'test name', - version: 'test version', - schemaUrl: 'test schema url', - }, + const instrumentationScope = { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', }; - const logRecord = new LogRecord(config, data || {}); - return { logRecord, config }; + const resource = Resource.default(); + const loggerProvider = new LoggerProvider({ resource }); + const logger = new Logger( + instrumentationScope, + { + logRecordLimits: limits, + }, + loggerProvider + ); + const logRecord = new LogRecord(logger, data || {}); + return { logger, logRecord, instrumentationScope, resource }; }; describe('LogRecord', () => { @@ -85,7 +76,7 @@ describe('LogRecord', () => { ); }); - it('should return ReadableLogRecord', () => { + it('should return LogRecord', () => { const logRecordData: logsAPI.LogRecord = { timestamp: new Date().getTime(), severityNumber: logsAPI.SeverityNumber.DEBUG, @@ -98,7 +89,10 @@ describe('LogRecord', () => { spanId: 'span id', traceFlags: 1, }; - const { logRecord, config } = setup(undefined, logRecordData); + const { logRecord, resource, instrumentationScope } = setup( + undefined, + logRecordData + ); assert.deepStrictEqual( logRecord.time, timeInputToHrTime(logRecordData.timestamp!) @@ -113,14 +107,14 @@ describe('LogRecord', () => { assert.deepStrictEqual(logRecord.traceId, logRecordData.traceId); assert.deepStrictEqual(logRecord.spanId, logRecordData.spanId); assert.deepStrictEqual(logRecord.traceFlags, logRecordData.traceFlags); - assert.deepStrictEqual(logRecord.resource, config.resource); + assert.deepStrictEqual(logRecord.resource, resource); assert.deepStrictEqual( logRecord.instrumentationScope, - config.instrumentationScope + instrumentationScope ); }); - it('should return ReadableLogRecord with attributes', () => { + it('should return LogRecord with attributes', () => { const logRecordData: logsAPI.LogRecord = { timestamp: new Date().getTime(), severityNumber: logsAPI.SeverityNumber.DEBUG, @@ -149,15 +143,6 @@ describe('LogRecord', () => { attr1: false, attr2: 123, }); - - logRecord.emit(); - // shouldn't add new attribute - logRecord.setAttribute('attr3', 'value3'); - assert.deepStrictEqual(logRecord.attributes, { - name: 'test name', - attr1: false, - attr2: 123, - }); }); }); @@ -271,30 +256,6 @@ describe('LogRecord', () => { }); }); - describe('emit', () => { - it('should be emit', () => { - const { logRecord, config } = setup(); - const callSpy = sinon.spy(config.activeProcessor, 'onEmit'); - logRecord.emit(); - assert.ok(callSpy.called); - }); - - it('should have emitted', () => { - const { logRecord } = setup(); - assert.strictEqual(logRecord.emitted, false); - logRecord.emit(); - assert.strictEqual(logRecord.emitted, true); - }); - - it('should be allow emit only once', () => { - const { logRecord, config } = setup(); - const callSpy = sinon.spy(config.activeProcessor, 'onEmit'); - logRecord.emit(); - logRecord.emit(); - assert.ok(callSpy.callCount === 1); - }); - }); - describe('log record processor', () => { it('should call onEmit synchronously when log record is emitted', () => { let emitted = false; diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts index d0155f7d9e..f6aed30018 100644 --- a/experimental/packages/sdk-logs/test/common/Logger.test.ts +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -16,27 +16,21 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; -import { Logger, LogRecord, NoopLogRecordProcessor } from '../../src'; +import { Logger, LoggerProvider } from '../../src'; import { loadDefaultConfig } from '../../src/config'; -import { MultiLogRecordProcessor } from '../../src/MultiLogRecordProcessor'; const setup = () => { const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const logger = new Logger({ - activeProcessor: new MultiLogRecordProcessor( - [new NoopLogRecordProcessor()], - forceFlushTimeoutMillis - ), - resource: Resource.default(), - logRecordLimits, - instrumentationScope: { + const logger = new Logger( + { name: 'test name', version: 'test version', schemaUrl: 'test schema url', }, - }); + { logRecordLimits }, + new LoggerProvider({ forceFlushTimeoutMillis }) + ); return { logger }; }; @@ -51,7 +45,7 @@ describe('Logger', () => { describe('emit', () => { it('should emit a logRecord instance', () => { const { logger } = setup(); - const callSpy = sinon.spy(LogRecord.prototype, 'emit'); + const callSpy = sinon.spy(logger.getActiveLogRecordProcessor(), 'onEmit'); logger.emit({ body: 'test log body', }); diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts index 0880dda30f..fa085fdebd 100644 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -47,7 +47,7 @@ describe('LoggerProvider', () => { assert.ok(provider instanceof LoggerProvider); }); - it('should use noop span processor by default and no diag error', () => { + it('should use noop log record processor by default and no diag error', () => { const errorStub = sinon.spy(diag, 'error'); const provider = new LoggerProvider(); const processors = provider.getActiveLogRecordProcessor().processors; @@ -136,7 +136,7 @@ describe('LoggerProvider', () => { }); describe('when attribute value length limit is defined via env', () => { - it('should have span attribute value length limit as deafult of Infinity', () => { + it('should have attribute value length limit as default of Infinity', () => { envSource.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'Infinity'; const logger = new LoggerProvider().getLogger('default'); const logRecordLimits = logger.getLogRecordLimits(); @@ -160,14 +160,14 @@ describe('LoggerProvider', () => { }); describe('when attribute count limit is defined via env', () => { - it('should have span and general attribute count limits as defined in env', () => { + it('should have attribute count limits as defined in env', () => { envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT = '35'; const logger = new LoggerProvider().getLogger('default'); const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 35); delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT; }); - it('should have span attribute count limit as default of 128', () => { + it('should have attribute count limit as default of 128', () => { envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT = '128'; const logger = new LoggerProvider().getLogger('default'); const logRecordLimits = logger.getLogRecordLimits(); @@ -270,7 +270,7 @@ describe('LoggerProvider', () => { }); }); - it('should throw error when calling forceFlush on all registered span processors fails', done => { + it('should throw error when calling forceFlush on all registered processors fails', done => { sinon.restore(); const forceFlushStub = sinon.stub( diff --git a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts index 9be369a2d7..48c631531e 100644 --- a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts @@ -28,8 +28,8 @@ import { MultiLogRecordProcessor } from './../../src/MultiLogRecordProcessor'; class TestProcessor implements LogRecordProcessor { logRecords: ReadableLogRecord[] = []; - onEmit(span: ReadableLogRecord): void { - this.logRecords.push(span); + onEmit(logRecord: ReadableLogRecord): void { + this.logRecords.push(logRecord); } shutdown(): Promise { this.logRecords = []; diff --git a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts index 855c52770c..2d0fab7a2c 100644 --- a/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/BatchLogRecordProcessor.test.ts @@ -16,7 +16,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { Resource } from '@opentelemetry/resources'; import { ExportResultCode, getEnv, @@ -27,12 +26,11 @@ import { import { BufferConfig, LogRecordLimits, - NoopLogRecordProcessor, LogRecord, InMemoryLogRecordExporter, + LoggerProvider, + Logger, } from '../../../src'; -import { loadDefaultConfig } from '../../../src/config'; -import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; import { BatchLogRecordProcessorBase } from '../../../src/export/BatchLogRecordProcessorBase'; class BatchLogRecordProcessor extends BatchLogRecordProcessorBase { @@ -41,30 +39,18 @@ class BatchLogRecordProcessor extends BatchLogRecordProcessorBase } const createLogRecord = (limits?: LogRecordLimits): LogRecord => { - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - - const logRecord = new LogRecord( + const logger = new Logger( + { + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', + }, { - activeProcessor: new MultiLogRecordProcessor( - [new NoopLogRecordProcessor()], - forceFlushTimeoutMillis - ), - resource: Resource.default(), - logRecordLimits: { - attributeValueLengthLimit: - limits?.attributeValueLengthLimit ?? - logRecordLimits.attributeValueLengthLimit, - attributeCountLimit: - limits?.attributeCountLimit ?? logRecordLimits.attributeCountLimit, - }, - instrumentationScope: { - name: 'test name', - version: 'test version', - schemaUrl: 'test schema url', - }, + logRecordLimits: limits, }, - {} + new LoggerProvider() ); + const logRecord = new LogRecord(logger, { body: 'body' }); return logRecord; }; diff --git a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts index c5f72ab8a2..202554dde9 100644 --- a/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/export/SimpleLogRecordProcessor.test.ts @@ -21,17 +21,15 @@ import { loggingErrorHandler, setGlobalErrorHandler, } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; import { InMemoryLogRecordExporter, LogRecordExporter, - NoopLogRecordProcessor, SimpleLogRecordProcessor, LogRecord, + LoggerProvider, + Logger, } from './../../../src'; -import { MultiLogRecordProcessor } from '../../../src/MultiLogRecordProcessor'; -import { loadDefaultConfig } from '../../../src/config'; const setup = (exporter: LogRecordExporter) => { const processor = new SimpleLogRecordProcessor(exporter); @@ -54,25 +52,18 @@ describe('SimpleLogRecordProcessor', () => { const { processor } = setup(exporter); assert.strictEqual(exporter.getFinishedLogRecords().length, 0); - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const logRecord = new LogRecord( + const logger = new Logger( { - activeProcessor: new MultiLogRecordProcessor( - [new NoopLogRecordProcessor()], - forceFlushTimeoutMillis - ), - resource: Resource.default(), - logRecordLimits, - instrumentationScope: { - name: 'test name', - version: 'test version', - schemaUrl: 'test schema url', - }, + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', }, - { - body: 'body', - } + {}, + new LoggerProvider() ); + const logRecord = new LogRecord(logger, { + body: 'body', + }); processor.onEmit(logRecord); assert.strictEqual(exporter.getFinishedLogRecords().length, 1); @@ -93,25 +84,18 @@ describe('SimpleLogRecordProcessor', () => { }; const { processor } = setup(exporter); - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); - const logRecord = new LogRecord( + const logger = new Logger( { - activeProcessor: new MultiLogRecordProcessor( - [new NoopLogRecordProcessor()], - forceFlushTimeoutMillis - ), - resource: Resource.default(), - logRecordLimits, - instrumentationScope: { - name: 'test name', - version: 'test version', - schemaUrl: 'test schema url', - }, + name: 'test name', + version: 'test version', + schemaUrl: 'test schema url', }, - { - body: 'body', - } + {}, + new LoggerProvider() ); + const logRecord = new LogRecord(logger, { + body: 'body', + }); const errorHandlerSpy = sinon.spy(); setGlobalErrorHandler(errorHandlerSpy); processor.onEmit(logRecord); From 03ff5116e816a8810807446d0b14b97016329362 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 1 Mar 2023 11:28:09 +0800 Subject: [PATCH 20/44] feat(sdk-logs): add browser test config --- .../packages/sdk-logs/test/index-webpack.ts | 20 +++++++++++++++++++ .../sdk-logs/test/index-webpack.worker.ts | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 experimental/packages/sdk-logs/test/index-webpack.ts create mode 100644 experimental/packages/sdk-logs/test/index-webpack.worker.ts diff --git a/experimental/packages/sdk-logs/test/index-webpack.ts b/experimental/packages/sdk-logs/test/index-webpack.ts new file mode 100644 index 0000000000..802d6c4053 --- /dev/null +++ b/experimental/packages/sdk-logs/test/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('./', true, /test$/); + testsContext.keys().forEach(testsContext); +} diff --git a/experimental/packages/sdk-logs/test/index-webpack.worker.ts b/experimental/packages/sdk-logs/test/index-webpack.worker.ts new file mode 100644 index 0000000000..802d6c4053 --- /dev/null +++ b/experimental/packages/sdk-logs/test/index-webpack.worker.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('./', true, /test$/); + testsContext.keys().forEach(testsContext); +} From a29f149c318c534df24f0152ea80cfc64f90c6b4 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 1 Mar 2023 15:37:23 +0800 Subject: [PATCH 21/44] feat: add test-utils compatible assert.rejects --- .../common/MultiLogRecordProcessor.test.ts | 3 +- .../packages/sdk-logs/test/test-utils.ts | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 experimental/packages/sdk-logs/test/test-utils.ts diff --git a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts index 48c631531e..856e1cf3c1 100644 --- a/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts +++ b/experimental/packages/sdk-logs/test/common/MultiLogRecordProcessor.test.ts @@ -25,6 +25,7 @@ import { } from './../../src'; import { loadDefaultConfig } from '../../src/config'; import { MultiLogRecordProcessor } from './../../src/MultiLogRecordProcessor'; +import { assertRejects } from '../test-utils'; class TestProcessor implements LogRecordProcessor { logRecords: ReadableLogRecord[] = []; @@ -176,7 +177,7 @@ describe('MultiLogRecordProcessor', () => { const res = multiProcessor.forceFlush(); clock.tick(forceFlushTimeoutMillis + 1000); clock.restore(); - await assert.rejects(res, /Operation timed out/); + await assertRejects(res, /Operation timed out/); }); }); diff --git a/experimental/packages/sdk-logs/test/test-utils.ts b/experimental/packages/sdk-logs/test/test-utils.ts new file mode 100644 index 0000000000..b9cacc0088 --- /dev/null +++ b/experimental/packages/sdk-logs/test/test-utils.ts @@ -0,0 +1,44 @@ +/* + * 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'; + +interface ErrorLikeConstructor { + new (): Error; +} + +/** + * Node.js v8.x and browser compatible `assert.rejects`. + */ +export async function assertRejects( + actual: any, + expected: RegExp | ErrorLikeConstructor +) { + let rejected; + try { + if (typeof actual === 'function') { + await actual(); + } else { + await actual; + } + } catch (err) { + rejected = true; + assert.throws(() => { + throw err; + }, expected); + } + assert(rejected, 'Promise not rejected'); +} From 7803d66009e173a7c21ef50f16c7c6e9685246a0 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 11:24:22 +0800 Subject: [PATCH 22/44] feat(sdk-logs): fix writing errors in README --- experimental/packages/sdk-logs/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/packages/sdk-logs/README.md b/experimental/packages/sdk-logs/README.md index daa86e1e2d..e68654f991 100644 --- a/experimental/packages/sdk-logs/README.md +++ b/experimental/packages/sdk-logs/README.md @@ -55,7 +55,7 @@ logger.emit({ ## Config -Logs configuration is a merge of user supplied configuration with both the default +Logs configuration is a merge of both the user supplied configuration and the default configuration as specified in [config.ts](./src/config.ts) ## Example @@ -76,4 +76,4 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [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/sdk-logs -[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fmetrics.svg +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fsdk%2Dlogs.svg From bb051eea68cb7e88694f9ec9638ecd21e12a2e74 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 11:27:38 +0800 Subject: [PATCH 23/44] feat(sdk-logs): update version to 0.36.1 --- experimental/packages/sdk-logs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 5f18460d54..1f0be3e9c3 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-logs", - "version": "0.35.1", + "version": "0.36.1", "publishConfig": { "access": "public" }, From 8bcb18dd7112fcc88249c5dba8d878e823d9d765 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 11:39:06 +0800 Subject: [PATCH 24/44] feat(sdk-logs): add examples --- .gitignore | 2 ++ experimental/examples/logs/README.md | 23 +++++++++++++++++++ experimental/examples/logs/index.js | 30 +++++++++++++++++++++++++ experimental/examples/logs/package.json | 13 +++++++++++ 4 files changed, 68 insertions(+) create mode 100644 experimental/examples/logs/README.md create mode 100644 experimental/examples/logs/index.js create mode 100644 experimental/examples/logs/package.json diff --git a/.gitignore b/.gitignore index 65c8373b08..90ef9bd3f2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ logs npm-debug.log* yarn-debug.log* yarn-error.log* +# Filter Logs singal files +!experimental/examples/logs # Runtime data pids diff --git a/experimental/examples/logs/README.md b/experimental/examples/logs/README.md new file mode 100644 index 0000000000..591d1eb5da --- /dev/null +++ b/experimental/examples/logs/README.md @@ -0,0 +1,23 @@ +## Installation + +```sh +# from this directory +npm install +``` + +## Run the Application + +LogRecord + +```sh +npm start +``` + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more information on OpenTelemetry logs, visit: + +## LICENSE + +Apache License 2.0 diff --git a/experimental/examples/logs/index.js b/experimental/examples/logs/index.js new file mode 100644 index 0000000000..53d47d604c --- /dev/null +++ b/experimental/examples/logs/index.js @@ -0,0 +1,30 @@ +'use strict'; + +const { DiagConsoleLogger, DiagLogLevel, diag } = require('@opentelemetry/api'); +const logsAPI = require('@opentelemetry/api-logs'); +const { SeverityNumber } = require('@opentelemetry/api-logs'); +const { + LoggerProvider, + ConsoleLogRecordExporter, + SimpleLogRecordProcessor, +} = require('@opentelemetry/sdk-logs'); + +// Optional and only needed to see the internal diagnostic logging (during development) +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); + +const loggerProvider = new LoggerProvider(); +loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()) +); + +logsAPI.logs.setGlobalLoggerProvider(loggerProvider); + +const logger = logsAPI.logs.getLogger('example', '1.0.0'); + +// emit a log record +logger.emit({ + severityNumber: SeverityNumber.INFO, + severityText: 'INFO', + body: 'this is a log record body', + attributes: { 'log.type': 'LogRecord' }, +}); diff --git a/experimental/examples/logs/package.json b/experimental/examples/logs/package.json new file mode 100644 index 0000000000..3b4309c8ed --- /dev/null +++ b/experimental/examples/logs/package.json @@ -0,0 +1,13 @@ +{ + "name": "logs-example", + "version": "0.1.0", + "private": true, + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/api-logs": "^0.36.1", + "@opentelemetry/sdk-logs": "^0.36.1" + } +} From 413025fa3a7d37696a615b0f1fa433b00ca020a9 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 14:23:33 +0800 Subject: [PATCH 25/44] feat(sdk-logs): fix LogRecord default timestamp to Date.now() --- experimental/packages/sdk-logs/src/LogRecord.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 3f07aeffb1..795b83ff65 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -16,12 +16,12 @@ import type { Attributes, AttributeValue } from '@opentelemetry/api'; import type * as logsAPI from '@opentelemetry/api-logs'; -import type { InstrumentationScope } from '@opentelemetry/core'; import * as api from '@opentelemetry/api'; import { - hrTime, timeInputToHrTime, isAttributeValue, + InstrumentationScope, + millisToHrTime, } from '@opentelemetry/core'; import type { IResource } from '@opentelemetry/resources'; @@ -44,7 +44,7 @@ export class LogRecord implements ReadableLogRecord { constructor(logger: Logger, logRecord: logsAPI.LogRecord) { const { - timestamp = hrTime(), + timestamp = Date.now(), severityNumber, severityText, body, From 314ff951e63564059344a787c9b5c10abc8d2e17 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 15:13:10 +0800 Subject: [PATCH 26/44] feat(sdk-logs): logRecord support rewrite time/body/severityNumber/newSeverityText --- .../packages/sdk-logs/src/LogRecord.ts | 29 +++++++-- .../sdk-logs/src/LogRecordProcessor.ts | 2 +- .../sdk-logs/test/common/LogRecord.test.ts | 64 ++++++++++++++++--- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index 795b83ff65..df627b7baf 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -21,7 +21,6 @@ import { timeInputToHrTime, isAttributeValue, InstrumentationScope, - millisToHrTime, } from '@opentelemetry/core'; import type { IResource } from '@opentelemetry/resources'; @@ -29,14 +28,14 @@ import type { ReadableLogRecord } from './export/ReadableLogRecord'; import type { LogRecordLimits } from './types'; import { Logger } from './Logger'; -export class LogRecord implements ReadableLogRecord { - readonly time: api.HrTime; +export class LogRecord implements logsAPI.LogRecord, ReadableLogRecord { + time: api.HrTime; + severityText?: string; + severityNumber?: logsAPI.SeverityNumber; + body?: string; readonly traceId?: string; readonly spanId?: string; readonly traceFlags?: number; - readonly severityText?: string; - readonly severityNumber?: logsAPI.SeverityNumber; - readonly body?: string; readonly resource: IResource; readonly instrumentationScope: InstrumentationScope; readonly attributes: Attributes = {}; @@ -95,6 +94,22 @@ export class LogRecord implements ReadableLogRecord { } } + public updateTime(time: api.TimeInput) { + this.time = timeInputToHrTime(time); + } + + public updateBody(body: string) { + this.body = body; + } + + public updateSeverityNumber(severityNumber: logsAPI.SeverityNumber) { + this.severityNumber = severityNumber; + } + + public updateSeverityText(severityText: string) { + this.severityText = severityText; + } + private _truncateToSize(value: AttributeValue): AttributeValue { const limit = this._logRecordLimits.attributeValueLengthLimit || 0; // Check limit @@ -124,6 +139,6 @@ export class LogRecord implements ReadableLogRecord { if (value.length <= limit) { return value; } - return value.substr(0, limit); + return value.substring(0, limit); } } diff --git a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts index 9cd6864882..efe075af05 100644 --- a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts @@ -24,7 +24,7 @@ export interface LogRecordProcessor { /** * Called when a {@link LogRecord} is emit - * @param logRecord the ReadableLogRecord that just emitted. + * @param logRecord the ReadWriteLogRecord that just emitted. */ onEmit(logRecord: LogRecord): void; diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts index a27fd42f37..5e2fb4a841 100644 --- a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts +++ b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts @@ -18,7 +18,11 @@ import type { Attributes, AttributeValue } from '@opentelemetry/api'; import * as logsAPI from '@opentelemetry/api-logs'; import type { HrTime } from '@opentelemetry/api'; import * as assert from 'assert'; -import { hrTimeToMilliseconds, timeInputToHrTime } from '@opentelemetry/core'; +import { + hrTimeToMilliseconds, + millisToHrTime, + timeInputToHrTime, +} from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { @@ -67,15 +71,6 @@ describe('LogRecord', () => { ); }); - it('should have a default timestamp', () => { - const { logRecord } = setup(); - assert.ok(logRecord.time !== undefined); - assert.ok( - hrTimeToMilliseconds(logRecord.time) > - hrTimeToMilliseconds(performanceTimeOrigin) - ); - }); - it('should return LogRecord', () => { const logRecordData: logsAPI.LogRecord = { timestamp: new Date().getTime(), @@ -256,6 +251,55 @@ describe('LogRecord', () => { }); }); + describe('should rewrite time/body/severityNumber/severityText', () => { + const currentTime = new Date().getTime(); + const logRecordData: logsAPI.LogRecord = { + timestamp: currentTime, + severityNumber: logsAPI.SeverityNumber.DEBUG, + severityText: 'DEBUG', + body: 'this is a body', + attributes: { + name: 'test name', + }, + traceId: 'trance id', + spanId: 'span id', + traceFlags: 1, + }; + + const newTime = millisToHrTime(currentTime + 1000); + const newBody = 'this is a new body'; + const newSeverityNumber = logsAPI.SeverityNumber.INFO; + const newSeverityText = 'INFO'; + + it('should rewrite directly through the property method', () => { + const { logRecord } = setup(undefined, logRecordData); + + logRecord.time = newTime; + logRecord.body = newBody; + logRecord.severityNumber = newSeverityNumber; + logRecord.severityText = newSeverityText; + + assert.deepStrictEqual(logRecord.time, newTime); + assert.deepStrictEqual(logRecord.body, newBody); + assert.deepStrictEqual(logRecord.severityNumber, newSeverityNumber); + assert.deepStrictEqual(logRecord.severityText, newSeverityText); + }); + + it('should rewrite using the update method', () => { + const { logRecord } = setup(undefined, logRecordData); + + logRecord.updateTime(newTime); + logRecord.updateBody(newBody); + logRecord.updateSeverityNumber(newSeverityNumber); + logRecord.updateSeverityText(newSeverityText); + + assert.deepStrictEqual(logRecord.time, newTime); + assert.deepStrictEqual(logRecord.body, newBody); + assert.deepStrictEqual(logRecord.severityNumber, newSeverityNumber); + assert.deepStrictEqual(logRecord.severityText, newSeverityText); + }); + }); + describe('log record processor', () => { it('should call onEmit synchronously when log record is emitted', () => { let emitted = false; From 8d476d2bda9389da4ad77a40823ff9fad4778a04 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 15:29:35 +0800 Subject: [PATCH 27/44] feat(sdk-logs): add logs processor environments --- experimental/packages/sdk-logs/src/config.ts | 8 ++++---- .../src/export/BatchLogRecordProcessorBase.ts | 8 ++++---- .../sdk-logs/test/common/LoggerProvider.test.ts | 12 ++++++------ .../opentelemetry-core/src/utils/environment.ts | 16 ++++++++++++---- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/experimental/packages/sdk-logs/src/config.ts b/experimental/packages/sdk-logs/src/config.ts index 2b2e12c3e3..b74ea4c5c0 100644 --- a/experimental/packages/sdk-logs/src/config.ts +++ b/experimental/packages/sdk-logs/src/config.ts @@ -27,8 +27,8 @@ export function loadDefaultConfig() { forceFlushTimeoutMillis: 30000, logRecordLimits: { attributeValueLengthLimit: - getEnv().OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, - attributeCountLimit: getEnv().OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT, + getEnv().OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, + attributeCountLimit: getEnv().OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT, }, }; } @@ -48,7 +48,7 @@ export function reconfigureLimits(userConfig: LoggerConfig): LoggerConfig { */ logRecordLimits.attributeCountLimit = userConfig.logRecordLimits?.attributeCountLimit ?? - parsedEnvConfig.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT ?? + parsedEnvConfig.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT ?? parsedEnvConfig.OTEL_ATTRIBUTE_COUNT_LIMIT ?? DEFAULT_ATTRIBUTE_COUNT_LIMIT; @@ -57,7 +57,7 @@ export function reconfigureLimits(userConfig: LoggerConfig): LoggerConfig { */ logRecordLimits.attributeValueLengthLimit = userConfig.logRecordLimits?.attributeValueLengthLimit ?? - parsedEnvConfig.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT ?? + parsedEnvConfig.OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT ?? parsedEnvConfig.OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT ?? DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT; diff --git a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts index f8a1331f09..92d42fe44b 100644 --- a/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts +++ b/experimental/packages/sdk-logs/src/export/BatchLogRecordProcessorBase.ts @@ -45,12 +45,12 @@ export abstract class BatchLogRecordProcessorBase constructor(private readonly _exporter: LogRecordExporter, config?: T) { const env = getEnv(); this._maxExportBatchSize = - config?.maxExportBatchSize ?? env.OTEL_BSP_MAX_EXPORT_BATCH_SIZE; - this._maxQueueSize = config?.maxQueueSize ?? env.OTEL_BSP_MAX_QUEUE_SIZE; + config?.maxExportBatchSize ?? env.OTEL_BLRP_MAX_EXPORT_BATCH_SIZE; + this._maxQueueSize = config?.maxQueueSize ?? env.OTEL_BLRP_MAX_QUEUE_SIZE; this._scheduledDelayMillis = - config?.scheduledDelayMillis ?? env.OTEL_BSP_SCHEDULE_DELAY; + config?.scheduledDelayMillis ?? env.OTEL_BLRP_SCHEDULE_DELAY; this._exportTimeoutMillis = - config?.exportTimeoutMillis ?? env.OTEL_BSP_EXPORT_TIMEOUT; + config?.exportTimeoutMillis ?? env.OTEL_BLRP_EXPORT_TIMEOUT; this._shutdownOnce = new BindOnceFuture(this._shutdown, this); diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts index fa085fdebd..7eb82f32c4 100644 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -137,14 +137,14 @@ describe('LoggerProvider', () => { describe('when attribute value length limit is defined via env', () => { it('should have attribute value length limit as default of Infinity', () => { - envSource.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'Infinity'; + envSource.OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'Infinity'; const logger = new LoggerProvider().getLogger('default'); const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual( logRecordLimits.attributeValueLengthLimit, Infinity ); - delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT; + delete envSource.OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT; }); }); @@ -161,18 +161,18 @@ describe('LoggerProvider', () => { describe('when attribute count limit is defined via env', () => { it('should have attribute count limits as defined in env', () => { - envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT = '35'; + envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = '35'; const logger = new LoggerProvider().getLogger('default'); const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 35); - delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT; + delete envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT; }); it('should have attribute count limit as default of 128', () => { - envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT = '128'; + envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = '128'; const logger = new LoggerProvider().getLogger('default'); const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 128); - delete envSource.OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT; + delete envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT; }); }); diff --git a/packages/opentelemetry-core/src/utils/environment.ts b/packages/opentelemetry-core/src/utils/environment.ts index eeb832d7cd..45fd67d9f3 100644 --- a/packages/opentelemetry-core/src/utils/environment.ts +++ b/packages/opentelemetry-core/src/utils/environment.ts @@ -41,12 +41,16 @@ const ENVIRONMENT_NUMBERS_KEYS = [ 'OTEL_BSP_MAX_EXPORT_BATCH_SIZE', 'OTEL_BSP_MAX_QUEUE_SIZE', 'OTEL_BSP_SCHEDULE_DELAY', + 'OTEL_BLRP_EXPORT_TIMEOUT', + 'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE', + 'OTEL_BLRP_MAX_QUEUE_SIZE', + 'OTEL_BLRP_SCHEDULE_DELAY', 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT', 'OTEL_ATTRIBUTE_COUNT_LIMIT', 'OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT', 'OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT', - 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT', - 'OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT', + 'OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT', + 'OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT', 'OTEL_SPAN_EVENT_COUNT_LIMIT', 'OTEL_SPAN_LINK_COUNT_LIMIT', 'OTEL_SPAN_ATTRIBUTE_PER_EVENT_COUNT_LIMIT', @@ -154,6 +158,10 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_BSP_MAX_EXPORT_BATCH_SIZE: 512, OTEL_BSP_MAX_QUEUE_SIZE: 2048, OTEL_BSP_SCHEDULE_DELAY: 5000, + OTEL_BLRP_EXPORT_TIMEOUT: 30000, + OTEL_BLRP_MAX_EXPORT_BATCH_SIZE: 512, + OTEL_BLRP_MAX_QUEUE_SIZE: 2048, + OTEL_BLRP_SCHEDULE_DELAY: 5000, OTEL_EXPORTER_JAEGER_AGENT_HOST: '', OTEL_EXPORTER_JAEGER_AGENT_PORT: 6832, OTEL_EXPORTER_JAEGER_ENDPOINT: '', @@ -178,9 +186,9 @@ export const DEFAULT_ENVIRONMENT: Required = { OTEL_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, - OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: + OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT: DEFAULT_ATTRIBUTE_VALUE_LENGTH_LIMIT, - OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, + OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT: DEFAULT_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT: 128, OTEL_SPAN_LINK_COUNT_LIMIT: 128, OTEL_SPAN_ATTRIBUTE_PER_EVENT_COUNT_LIMIT: From e579b13422e1095a19f619b98d79862c5735b0ea Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 15:34:52 +0800 Subject: [PATCH 28/44] feat(sdk-logs): modify export style --- experimental/packages/sdk-logs/src/index.ts | 29 +++++++++++-------- .../sdk-logs/src/platform/browser/index.ts | 2 +- .../packages/sdk-logs/src/platform/index.ts | 2 +- .../sdk-logs/src/platform/node/index.ts | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/experimental/packages/sdk-logs/src/index.ts b/experimental/packages/sdk-logs/src/index.ts index 11dcb82f20..60124da1f0 100644 --- a/experimental/packages/sdk-logs/src/index.ts +++ b/experimental/packages/sdk-logs/src/index.ts @@ -14,15 +14,20 @@ * limitations under the License. */ -export * from './types'; -export * from './LoggerProvider'; -export * from './Logger'; -export * from './LogRecord'; -export * from './LogRecordProcessor'; -export * from './export/ReadableLogRecord'; -export * from './export/NoopLogRecordProcessor'; -export * from './export/ConsoleLogRecordExporter'; -export * from './export/LogRecordExporter'; -export * from './export/SimpleLogRecordProcessor'; -export * from './export/InMemoryLogRecordExporter'; -export * from './platform'; +export { + LoggerConfig, + LogRecordLimits, + BufferConfig, + BatchLogRecordProcessorBrowserConfig, +} from './types'; +export { LoggerProvider } from './LoggerProvider'; +export { Logger } from './Logger'; +export { LogRecord } from './LogRecord'; +export { LogRecordProcessor } from './LogRecordProcessor'; +export { ReadableLogRecord } from './export/ReadableLogRecord'; +export { NoopLogRecordProcessor } from './export/NoopLogRecordProcessor'; +export { ConsoleLogRecordExporter } from './export/ConsoleLogRecordExporter'; +export { LogRecordExporter } from './export/LogRecordExporter'; +export { SimpleLogRecordProcessor } from './export/SimpleLogRecordProcessor'; +export { InMemoryLogRecordExporter } from './export/InMemoryLogRecordExporter'; +export { BatchLogRecordProcessor } from './platform'; diff --git a/experimental/packages/sdk-logs/src/platform/browser/index.ts b/experimental/packages/sdk-logs/src/platform/browser/index.ts index 6467530229..81d097ddf0 100644 --- a/experimental/packages/sdk-logs/src/platform/browser/index.ts +++ b/experimental/packages/sdk-logs/src/platform/browser/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './export/BatchLogRecordProcessor'; +export { BatchLogRecordProcessor } from './export/BatchLogRecordProcessor'; diff --git a/experimental/packages/sdk-logs/src/platform/index.ts b/experimental/packages/sdk-logs/src/platform/index.ts index cdaf8858ce..e4b0e0d7f8 100644 --- a/experimental/packages/sdk-logs/src/platform/index.ts +++ b/experimental/packages/sdk-logs/src/platform/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './node'; +export { BatchLogRecordProcessor } from './node'; diff --git a/experimental/packages/sdk-logs/src/platform/node/index.ts b/experimental/packages/sdk-logs/src/platform/node/index.ts index 6467530229..81d097ddf0 100644 --- a/experimental/packages/sdk-logs/src/platform/node/index.ts +++ b/experimental/packages/sdk-logs/src/platform/node/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './export/BatchLogRecordProcessor'; +export { BatchLogRecordProcessor } from './export/BatchLogRecordProcessor'; From ab3b8618867f256de122e2fe0099712703a6d4ef Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 15:51:57 +0800 Subject: [PATCH 29/44] feat(sdk-logs): update version to 0.36.1 --- experimental/packages/sdk-logs/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 1f0be3e9c3..5314b010be 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -68,7 +68,7 @@ ], "sideEffects": false, "devDependencies": { - "@opentelemetry/api": "^1.4.0", + "@opentelemetry/api": "^1.4.1", "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", @@ -87,11 +87,11 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.4.0" + "@opentelemetry/api": "^1.4.1" }, "dependencies": { - "@opentelemetry/core": "1.9.1", - "@opentelemetry/resources": "1.9.1", - "@opentelemetry/api-logs": "0.35.1" + "@opentelemetry/core": "^1.10.1", + "@opentelemetry/resources": "^1.10.1", + "@opentelemetry/api-logs": "^0.36.1" } } From 2501941891616fa0783ffa55dc4daa899b8fa586 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 16:02:30 +0800 Subject: [PATCH 30/44] feat(sdk-logs): remove exporter factory --- .../packages/sdk-logs/src/LoggerProvider.ts | 48 +++---------------- .../test/common/LoggerProvider.test.ts | 11 +---- 2 files changed, 7 insertions(+), 52 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index abeec3e3de..09251ac989 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -16,25 +16,16 @@ import { diag } from '@opentelemetry/api'; import type * as logsAPI from '@opentelemetry/api-logs'; import { IResource, Resource } from '@opentelemetry/resources'; -import { getEnv, merge } from '@opentelemetry/core'; +import { merge } from '@opentelemetry/core'; import type { LoggerConfig } from './types'; import type { LogRecordProcessor } from './LogRecordProcessor'; -import type { LogRecordExporter } from './export/LogRecordExporter'; import { Logger } from './Logger'; import { loadDefaultConfig, reconfigureLimits } from './config'; import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; -import { BatchLogRecordProcessor } from './platform/node/export/BatchLogRecordProcessor'; import { NoopLogRecordProcessor } from './export/NoopLogRecordProcessor'; -export type EXPORTER_FACTORY = () => LogRecordExporter; - export class LoggerProvider implements logsAPI.LoggerProvider { - protected static readonly _registeredExporters = new Map< - string, - EXPORTER_FACTORY - >(); - public readonly resource: IResource; private readonly _loggers: Map = new Map(); @@ -55,18 +46,11 @@ export class LoggerProvider implements logsAPI.LoggerProvider { forceFlushTimeoutMillis, }; - const defaultExporter = this._buildExporterFromEnv(); - if (defaultExporter !== undefined) { - this._activeProcessor = new MultiLogRecordProcessor( - [new BatchLogRecordProcessor(defaultExporter)], - forceFlushTimeoutMillis - ); - } else { - this._activeProcessor = new MultiLogRecordProcessor( - [new NoopLogRecordProcessor()], - forceFlushTimeoutMillis - ); - } + // add a default processor: NoopLogRecordProcessor + this._activeProcessor = new MultiLogRecordProcessor( + [new NoopLogRecordProcessor()], + forceFlushTimeoutMillis + ); } /** @@ -138,24 +122,4 @@ export class LoggerProvider implements logsAPI.LoggerProvider { public getActiveLoggers(): Map { return this._loggers; } - - protected _getLogRecordExporter(name: string): LogRecordExporter | undefined { - return (this.constructor as typeof LoggerProvider)._registeredExporters.get( - name - )?.(); - } - - protected _buildExporterFromEnv(): LogRecordExporter | undefined { - const exporterName = getEnv().OTEL_LOGS_EXPORTER; - if (exporterName === 'none' || exporterName === '') { - return; - } - const exporter = this._getLogRecordExporter(exporterName); - if (!exporter) { - diag.error( - `Exporter "${exporterName}" requested through environment variable is unavailable.` - ); - } - return exporter; - } } diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts index 7eb82f32c4..04e5f51319 100644 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -73,20 +73,11 @@ describe('LoggerProvider', () => { }); describe('when user sets unavailable exporter', () => { - it('should use noop log record processor by default and show diag error', () => { - const errorStub = sinon.spy(diag, 'error'); - envSource.OTEL_LOGS_EXPORTER = 'someExporter'; - + it('should use noop log record processor by default', () => { const provider = new LoggerProvider(); const processors = provider.getActiveLogRecordProcessor().processors; assert.ok(processors.length === 1); assert.ok(processors[0] instanceof NoopLogRecordProcessor); - - sinon.assert.calledWith( - errorStub, - 'Exporter "someExporter" requested through environment variable is unavailable.' - ); - delete envSource.OTEL_LOGS_EXPORTER; }); }); From c0c13200ffdc9062b0d539c6c7051a63034b645a Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 16:39:51 +0800 Subject: [PATCH 31/44] feat(sdk-logs): update CHANGELOG --- experimental/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index fb483c7172..dfb12deba0 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -10,6 +10,9 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(sdk-logs): logs sdk implementation. [#3549](https://github.com/open-telemetry/opentelemetry-js/pull/3549/) @fuaiyi +* feat(core): add logs environment variables; add timeout utils method. [#3549](https://github.com/open-telemetry/opentelemetry-js/pull/3549/) @fuaiyi + ### :bug: (Bug Fix) ### :books: (Refine Doc) From 14b1a5537fca72622fcf0865ab316d8fccf01397 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 21 Mar 2023 16:53:53 +0800 Subject: [PATCH 32/44] feat(sdk-logs): change the processing of schemeUrl --- experimental/packages/sdk-logs/src/LoggerProvider.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 09251ac989..2924c4c0ca 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -61,12 +61,15 @@ export class LoggerProvider implements logsAPI.LoggerProvider { version?: string, options?: logsAPI.LoggerOptions ): Logger { - const { schemaUrl = '' } = options || {}; - const key = `${name}@${version || ''}:${schemaUrl}`; + const key = `${name}@${version || ''}:${options?.schemaUrl || ''}`; if (!this._loggers.has(key)) { this._loggers.set( key, - new Logger({ name, version, schemaUrl }, this._config, this) + new Logger( + { name, version, schemaUrl: options?.schemaUrl }, + this._config, + this + ) ); } return this._loggers.get(key)!; From e8ced536a7c7d47b7b7ee1b0e24e3ffabc64e6b4 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 28 Mar 2023 16:35:39 +0800 Subject: [PATCH 33/44] feat(sdk-logs): split LoggerProviderConfig and LoggerConfig --- experimental/packages/sdk-logs/src/LoggerProvider.ts | 6 +++--- experimental/packages/sdk-logs/src/types.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 2924c4c0ca..f62f5261d7 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -18,7 +18,7 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import { IResource, Resource } from '@opentelemetry/resources'; import { merge } from '@opentelemetry/core'; -import type { LoggerConfig } from './types'; +import type { LoggerProviderConfig } from './types'; import type { LogRecordProcessor } from './LogRecordProcessor'; import { Logger } from './Logger'; import { loadDefaultConfig, reconfigureLimits } from './config'; @@ -31,9 +31,9 @@ export class LoggerProvider implements logsAPI.LoggerProvider { private readonly _loggers: Map = new Map(); private _activeProcessor: MultiLogRecordProcessor; private readonly _registeredLogRecordProcessors: LogRecordProcessor[] = []; - private readonly _config: LoggerConfig; + private readonly _config: LoggerProviderConfig; - constructor(config: LoggerConfig = {}) { + constructor(config: LoggerProviderConfig = {}) { const { resource = Resource.empty(), logRecordLimits, diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index 34246d577d..f6e1abc436 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -16,10 +16,7 @@ import type { IResource } from '@opentelemetry/resources'; -export interface LoggerConfig { - /** Log Record Limits*/ - logRecordLimits?: LogRecordLimits; - +export interface LoggerProviderConfig extends LoggerConfig { /** Resource associated with trace telemetry */ resource?: IResource; @@ -30,6 +27,11 @@ export interface LoggerConfig { forceFlushTimeoutMillis?: number; } +export interface LoggerConfig { + /** Log Record Limits*/ + logRecordLimits?: LogRecordLimits; +} + export interface LogRecordLimits { /** attributeValueLengthLimit is maximum allowed attribute value size */ attributeValueLengthLimit?: number; From 8363356f3ae35d2dbadcfaac90d881a6fb4073e7 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 28 Mar 2023 20:35:19 +0800 Subject: [PATCH 34/44] feat(sdk-logs): getLogger with default name when name is invalid --- experimental/packages/sdk-logs/src/LoggerProvider.ts | 10 ++++++++-- .../sdk-logs/test/common/LoggerProvider.test.ts | 7 +++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index f62f5261d7..61b551f28c 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -25,6 +25,8 @@ import { loadDefaultConfig, reconfigureLimits } from './config'; import { MultiLogRecordProcessor } from './MultiLogRecordProcessor'; import { NoopLogRecordProcessor } from './export/NoopLogRecordProcessor'; +export const DEFAULT_LOGGER_NAME = 'unknown'; + export class LoggerProvider implements logsAPI.LoggerProvider { public readonly resource: IResource; @@ -61,12 +63,16 @@ export class LoggerProvider implements logsAPI.LoggerProvider { version?: string, options?: logsAPI.LoggerOptions ): Logger { - const key = `${name}@${version || ''}:${options?.schemaUrl || ''}`; + if (!name) { + diag.warn('Logger requested without instrumentation scope name.'); + } + const loggerName = name || DEFAULT_LOGGER_NAME; + const key = `${loggerName}@${version || ''}:${options?.schemaUrl || ''}`; if (!this._loggers.has(key)) { this._loggers.set( key, new Logger( - { name, version, schemaUrl: options?.schemaUrl }, + { name: loggerName, version, schemaUrl: options?.schemaUrl }, this._config, this ) diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts index 04e5f51319..2b7c06e451 100644 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -21,6 +21,7 @@ import * as sinon from 'sinon'; import { Logger, LoggerProvider, NoopLogRecordProcessor } from '../../src'; import { loadDefaultConfig } from '../../src/config'; +import { DEFAULT_LOGGER_NAME } from './../../src/LoggerProvider'; describe('LoggerProvider', () => { let envSource: Record; @@ -182,6 +183,12 @@ describe('LoggerProvider', () => { const testVersion = 'test version'; const testSchemaURL = 'test schema url'; + it('should create a logger instance with default name if the name is invalid ', () => { + const provider = new LoggerProvider(); + const logger = provider.getLogger(''); + assert.ok(logger.instrumentationScope.name === DEFAULT_LOGGER_NAME); + }); + it("should create a logger instance if the name doesn't exist", () => { const provider = new LoggerProvider(); assert.ok(provider.getActiveLoggers().size === 0); From b7e399efd7c6f284ce989b2972f9e879edf386b6 Mon Sep 17 00:00:00 2001 From: fugao Date: Tue, 28 Mar 2023 22:43:25 +0800 Subject: [PATCH 35/44] feat(sdk-logs): improve the shutdown logic of LoggerProvider --- .../packages/api-logs/src/NoopLogger.ts | 2 + experimental/packages/api-logs/src/index.ts | 2 + .../packages/sdk-logs/src/LoggerProvider.ts | 28 ++++++++- .../test/common/LoggerProvider.test.ts | 60 +++++++++++++++---- 4 files changed, 78 insertions(+), 14 deletions(-) diff --git a/experimental/packages/api-logs/src/NoopLogger.ts b/experimental/packages/api-logs/src/NoopLogger.ts index dab79439ee..b3d092167f 100644 --- a/experimental/packages/api-logs/src/NoopLogger.ts +++ b/experimental/packages/api-logs/src/NoopLogger.ts @@ -20,3 +20,5 @@ import { LogRecord } from './types/LogRecord'; export class NoopLogger implements Logger { emit(_logRecord: LogRecord): void {} } + +export const NOOP_LOGGER = new NoopLogger(); diff --git a/experimental/packages/api-logs/src/index.ts b/experimental/packages/api-logs/src/index.ts index ce158ba323..34b4d8e20e 100644 --- a/experimental/packages/api-logs/src/index.ts +++ b/experimental/packages/api-logs/src/index.ts @@ -18,6 +18,8 @@ export * from './types/Logger'; export * from './types/LoggerProvider'; export * from './types/LogRecord'; export * from './types/LoggerOptions'; +export * from './NoopLogger'; +export * from './NoopLoggerProvider'; import { LogsAPI } from './api/logs'; export const logs = LogsAPI.getInstance(); diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 61b551f28c..76694aaa22 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -15,8 +15,9 @@ */ import { diag } from '@opentelemetry/api'; import type * as logsAPI from '@opentelemetry/api-logs'; +import { NOOP_LOGGER } from '@opentelemetry/api-logs'; import { IResource, Resource } from '@opentelemetry/resources'; -import { merge } from '@opentelemetry/core'; +import { BindOnceFuture, merge } from '@opentelemetry/core'; import type { LoggerProviderConfig } from './types'; import type { LogRecordProcessor } from './LogRecordProcessor'; @@ -34,6 +35,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { private _activeProcessor: MultiLogRecordProcessor; private readonly _registeredLogRecordProcessors: LogRecordProcessor[] = []; private readonly _config: LoggerProviderConfig; + private _shutdownOnce: BindOnceFuture; constructor(config: LoggerProviderConfig = {}) { const { @@ -48,6 +50,8 @@ export class LoggerProvider implements logsAPI.LoggerProvider { forceFlushTimeoutMillis, }; + this._shutdownOnce = new BindOnceFuture(this._shutdown, this); + // add a default processor: NoopLogRecordProcessor this._activeProcessor = new MultiLogRecordProcessor( [new NoopLogRecordProcessor()], @@ -62,7 +66,12 @@ export class LoggerProvider implements logsAPI.LoggerProvider { name: string, version?: string, options?: logsAPI.LoggerOptions - ): Logger { + ): logsAPI.Logger { + if (this._shutdownOnce.isCalled) { + diag.warn('A shutdown LoggerProvider cannot provide a Meter'); + return NOOP_LOGGER; + } + if (!name) { diag.warn('Logger requested without instrumentation scope name.'); } @@ -111,6 +120,11 @@ export class LoggerProvider implements logsAPI.LoggerProvider { * Returns a promise which is resolved when all flushes are complete. */ public forceFlush(): Promise { + // do not flush after shutdown + if (this._shutdownOnce.isCalled) { + diag.warn('invalid attempt to force flush after LoggerProvider shutdown'); + return this._shutdownOnce.promise; + } return this._activeProcessor.forceFlush(); } @@ -121,7 +135,11 @@ export class LoggerProvider implements logsAPI.LoggerProvider { * Returns a promise which is resolved when all flushes are complete. */ public shutdown(): Promise { - return this._activeProcessor.shutdown(); + if (this._shutdownOnce.isCalled) { + diag.warn('shutdown may only be called once per LoggerProvider'); + return this._shutdownOnce.promise; + } + return this._shutdownOnce.call(); } public getActiveLogRecordProcessor(): MultiLogRecordProcessor { @@ -131,4 +149,8 @@ export class LoggerProvider implements logsAPI.LoggerProvider { public getActiveLoggers(): Map { return this._loggers; } + + private _shutdown(): Promise { + return this._activeProcessor.shutdown(); + } } diff --git a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts index 2b7c06e451..d6032515a7 100644 --- a/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts +++ b/experimental/packages/sdk-logs/test/common/LoggerProvider.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { logs } from '@opentelemetry/api-logs'; +import { logs, NoopLogger } from '@opentelemetry/api-logs'; import { diag } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; @@ -85,7 +85,7 @@ describe('LoggerProvider', () => { describe('logRecordLimits', () => { describe('when not defined default values', () => { it('should have logger with default values', () => { - const logger = new LoggerProvider({}).getLogger('default'); + const logger = new LoggerProvider({}).getLogger('default') as Logger; assert.deepStrictEqual(logger.getLogRecordLimits(), { attributeValueLengthLimit: Infinity, attributeCountLimit: 128, @@ -99,7 +99,7 @@ describe('LoggerProvider', () => { logRecordLimits: { attributeCountLimit: 100, }, - }).getLogger('default'); + }).getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 100); }); @@ -111,7 +111,7 @@ describe('LoggerProvider', () => { logRecordLimits: { attributeValueLengthLimit: 10, }, - }).getLogger('default'); + }).getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeValueLengthLimit, 10); }); @@ -121,7 +121,7 @@ describe('LoggerProvider', () => { logRecordLimits: { attributeValueLengthLimit: -10, }, - }).getLogger('default'); + }).getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeValueLengthLimit, -10); }); @@ -130,7 +130,7 @@ describe('LoggerProvider', () => { describe('when attribute value length limit is defined via env', () => { it('should have attribute value length limit as default of Infinity', () => { envSource.OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT = 'Infinity'; - const logger = new LoggerProvider().getLogger('default'); + const logger = new LoggerProvider().getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual( logRecordLimits.attributeValueLengthLimit, @@ -142,7 +142,7 @@ describe('LoggerProvider', () => { describe('when attribute value length limit is not defined via env', () => { it('should use default value of Infinity', () => { - const logger = new LoggerProvider().getLogger('default'); + const logger = new LoggerProvider().getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual( logRecordLimits.attributeValueLengthLimit, @@ -154,14 +154,14 @@ describe('LoggerProvider', () => { describe('when attribute count limit is defined via env', () => { it('should have attribute count limits as defined in env', () => { envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = '35'; - const logger = new LoggerProvider().getLogger('default'); + const logger = new LoggerProvider().getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 35); delete envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT; }); it('should have attribute count limit as default of 128', () => { envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT = '128'; - const logger = new LoggerProvider().getLogger('default'); + const logger = new LoggerProvider().getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 128); delete envSource.OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT; @@ -170,7 +170,7 @@ describe('LoggerProvider', () => { describe('when attribute count limit is not defined via env', () => { it('should use default value of 128', () => { - const logger = new LoggerProvider().getLogger('default'); + const logger = new LoggerProvider().getLogger('default') as Logger; const logRecordLimits = logger.getLogRecordLimits(); assert.strictEqual(logRecordLimits.attributeCountLimit, 128); }); @@ -185,7 +185,7 @@ describe('LoggerProvider', () => { it('should create a logger instance with default name if the name is invalid ', () => { const provider = new LoggerProvider(); - const logger = provider.getLogger(''); + const logger = provider.getLogger('') as Logger; assert.ok(logger.instrumentationScope.name === DEFAULT_LOGGER_NAME); }); @@ -308,5 +308,43 @@ describe('LoggerProvider', () => { provider.shutdown(); sinon.assert.calledOnce(shutdownStub); }); + + it('get a noop logger on shutdown', () => { + const provider = new LoggerProvider(); + provider.shutdown(); + const logger = provider.getLogger('default', '1.0.0'); + // returned tracer should be no-op, not instance of Logger (from SDK) + assert.ok(logger instanceof NoopLogger); + }); + + it('should not force flush on shutdown', () => { + const provider = new LoggerProvider(); + const logRecordProcessor = new NoopLogRecordProcessor(); + provider.addLogRecordProcessor(logRecordProcessor); + const forceFlushStub = sinon.stub( + provider.getActiveLogRecordProcessor(), + 'forceFlush' + ); + const warnStub = sinon.spy(diag, 'warn'); + provider.shutdown(); + provider.forceFlush(); + sinon.assert.notCalled(forceFlushStub); + sinon.assert.calledOnce(warnStub); + }); + + it('should not shutdown on shutdown', () => { + const provider = new LoggerProvider(); + const logRecordProcessor = new NoopLogRecordProcessor(); + provider.addLogRecordProcessor(logRecordProcessor); + const shutdownStub = sinon.stub( + provider.getActiveLogRecordProcessor(), + 'shutdown' + ); + const warnStub = sinon.spy(diag, 'warn'); + provider.shutdown(); + provider.shutdown(); + sinon.assert.calledOnce(shutdownStub); + sinon.assert.calledOnce(warnStub); + }); }); }); From b43efff5a027c946eb2fb8eeec24e6d74406cf8f Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 29 Mar 2023 11:06:51 +0800 Subject: [PATCH 36/44] feat(sdk-logs): improve the shutdown logic of LoggerProvider --- experimental/packages/sdk-logs/src/LoggerProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index 76694aaa22..b2d45b5716 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -68,7 +68,7 @@ export class LoggerProvider implements logsAPI.LoggerProvider { options?: logsAPI.LoggerOptions ): logsAPI.Logger { if (this._shutdownOnce.isCalled) { - diag.warn('A shutdown LoggerProvider cannot provide a Meter'); + diag.warn('A shutdown LoggerProvider cannot provide a Logger'); return NOOP_LOGGER; } From f42369366ba5d2b7ceee4026a6901ba2391053cb Mon Sep 17 00:00:00 2001 From: fugao Date: Sun, 9 Apr 2023 20:02:37 +0800 Subject: [PATCH 37/44] feat(sdk-logs): make log record read-only after it has been emitted --- .../packages/sdk-logs/src/LogRecord.ts | 82 +++++++++++++--- experimental/packages/sdk-logs/src/Logger.ts | 5 + .../sdk-logs/test/common/LogRecord.test.ts | 98 +++++++++++++++---- .../sdk-logs/test/common/Logger.test.ts | 11 ++- 4 files changed, 162 insertions(+), 34 deletions(-) diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index df627b7baf..a611a4445b 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Attributes, AttributeValue } from '@opentelemetry/api'; +import { Attributes, AttributeValue, diag } from '@opentelemetry/api'; import type * as logsAPI from '@opentelemetry/api-logs'; import * as api from '@opentelemetry/api'; import { @@ -29,18 +29,49 @@ import type { LogRecordLimits } from './types'; import { Logger } from './Logger'; export class LogRecord implements logsAPI.LogRecord, ReadableLogRecord { - time: api.HrTime; - severityText?: string; - severityNumber?: logsAPI.SeverityNumber; - body?: string; + readonly time: api.HrTime; readonly traceId?: string; readonly spanId?: string; readonly traceFlags?: number; readonly resource: IResource; readonly instrumentationScope: InstrumentationScope; readonly attributes: Attributes = {}; + private _severityText?: string; + private _severityNumber?: logsAPI.SeverityNumber; + private _body?: string; + private _isReadonly: boolean = false; private readonly _logRecordLimits: LogRecordLimits; + set severityText(severityText: string | undefined) { + if (this._isLogRecordReadonly()) { + return; + } + this._severityText = severityText; + } + get severityText(): string | undefined { + return this._severityText; + } + + set severityNumber(severityNumber: logsAPI.SeverityNumber | undefined) { + if (this._isLogRecordReadonly()) { + return; + } + this._severityNumber = severityNumber; + } + get severityNumber(): logsAPI.SeverityNumber | undefined { + return this._severityNumber; + } + + set body(body: string | undefined) { + if (this._isLogRecordReadonly()) { + return; + } + this._body = body; + } + get body(): string | undefined { + return this._body; + } + constructor(logger: Logger, logRecord: logsAPI.LogRecord) { const { timestamp = Date.now(), @@ -67,47 +98,59 @@ export class LogRecord implements logsAPI.LogRecord, ReadableLogRecord { } public setAttribute(key: string, value?: AttributeValue) { + if (this._isLogRecordReadonly()) { + return this; + } if (value === null) { - return; + return this; } if (key.length === 0) { api.diag.warn(`Invalid attribute key: ${key}`); - return; + return this; } if (!isAttributeValue(value)) { api.diag.warn(`Invalid attribute value set for key: ${key}`); - return; + return this; } if ( Object.keys(this.attributes).length >= this._logRecordLimits.attributeCountLimit! && !Object.prototype.hasOwnProperty.call(this.attributes, key) ) { - return; + return this; } this.attributes[key] = this._truncateToSize(value); + return this; } public setAttributes(attributes: Attributes) { for (const [k, v] of Object.entries(attributes)) { this.setAttribute(k, v); } + return this; } - public updateTime(time: api.TimeInput) { - this.time = timeInputToHrTime(time); - } - - public updateBody(body: string) { + public setBody(body: string) { this.body = body; + return this; } - public updateSeverityNumber(severityNumber: logsAPI.SeverityNumber) { + public setSeverityNumber(severityNumber: logsAPI.SeverityNumber) { this.severityNumber = severityNumber; + return this; } - public updateSeverityText(severityText: string) { + public setSeverityText(severityText: string) { this.severityText = severityText; + return this; + } + + /** + * A LogRecordProcessor may freely modify logRecord for the duration of the OnEmit call. + * If logRecord is needed after OnEmit returns (i.e. for asynchronous processing) only reads are permitted. + */ + public makeReadonly() { + this._isReadonly = true; } private _truncateToSize(value: AttributeValue): AttributeValue { @@ -141,4 +184,11 @@ export class LogRecord implements logsAPI.LogRecord, ReadableLogRecord { } return value.substring(0, limit); } + + private _isLogRecordReadonly(): boolean { + if (this._isReadonly) { + diag.warn('Can not execute the operation on emitted log record'); + } + return this._isReadonly; + } } diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index cacefbe426..7859309508 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -41,6 +41,11 @@ export class Logger implements logsAPI.Logger { public emit(logRecord: logsAPI.LogRecord): void { const logRecordInstance = new LogRecord(this, logRecord); this.getActiveLogRecordProcessor().onEmit(logRecordInstance); + /** + * A LogRecordProcessor may freely modify logRecord for the duration of the OnEmit call. + * If logRecord is needed after OnEmit returns (i.e. for asynchronous processing) only reads are permitted. + */ + logRecordInstance.makeReadonly(); } public getLogRecordLimits(): LogRecordLimits { diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts index 5e2fb4a841..3e1a2c6a08 100644 --- a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts +++ b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -import type { Attributes, AttributeValue } from '@opentelemetry/api'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { Attributes, AttributeValue, diag } from '@opentelemetry/api'; import * as logsAPI from '@opentelemetry/api-logs'; import type { HrTime } from '@opentelemetry/api'; -import * as assert from 'assert'; -import { - hrTimeToMilliseconds, - millisToHrTime, - timeInputToHrTime, -} from '@opentelemetry/core'; +import { hrTimeToMilliseconds, timeInputToHrTime } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; import { @@ -251,7 +248,7 @@ describe('LogRecord', () => { }); }); - describe('should rewrite time/body/severityNumber/severityText', () => { + describe('should rewrite body/severityNumber/severityText', () => { const currentTime = new Date().getTime(); const logRecordData: logsAPI.LogRecord = { timestamp: currentTime, @@ -266,7 +263,6 @@ describe('LogRecord', () => { traceFlags: 1, }; - const newTime = millisToHrTime(currentTime + 1000); const newBody = 'this is a new body'; const newSeverityNumber = logsAPI.SeverityNumber.INFO; const newSeverityText = 'INFO'; @@ -274,32 +270,100 @@ describe('LogRecord', () => { it('should rewrite directly through the property method', () => { const { logRecord } = setup(undefined, logRecordData); - logRecord.time = newTime; logRecord.body = newBody; logRecord.severityNumber = newSeverityNumber; logRecord.severityText = newSeverityText; - assert.deepStrictEqual(logRecord.time, newTime); assert.deepStrictEqual(logRecord.body, newBody); assert.deepStrictEqual(logRecord.severityNumber, newSeverityNumber); assert.deepStrictEqual(logRecord.severityText, newSeverityText); }); - it('should rewrite using the update method', () => { + it('should rewrite using the set method', () => { const { logRecord } = setup(undefined, logRecordData); - logRecord.updateTime(newTime); - logRecord.updateBody(newBody); - logRecord.updateSeverityNumber(newSeverityNumber); - logRecord.updateSeverityText(newSeverityText); + logRecord.setBody(newBody); + logRecord.setSeverityNumber(newSeverityNumber); + logRecord.setSeverityText(newSeverityText); - assert.deepStrictEqual(logRecord.time, newTime); assert.deepStrictEqual(logRecord.body, newBody); assert.deepStrictEqual(logRecord.severityNumber, newSeverityNumber); assert.deepStrictEqual(logRecord.severityText, newSeverityText); }); }); + describe('should be read-only(body/severityNumber/severityText) if makeReadonly has been called', () => { + const currentTime = new Date().getTime(); + const logRecordData: logsAPI.LogRecord = { + timestamp: currentTime, + severityNumber: logsAPI.SeverityNumber.DEBUG, + severityText: 'DEBUG', + body: 'this is a body', + attributes: { + name: 'test name', + }, + traceId: 'trance id', + spanId: 'span id', + traceFlags: 1, + }; + + const newBody = 'this is a new body'; + const newSeverityNumber = logsAPI.SeverityNumber.INFO; + const newSeverityText = 'INFO'; + + it('should not rewrite directly through the property method', () => { + const warnStub = sinon.spy(diag, 'warn'); + const { logRecord } = setup(undefined, logRecordData); + logRecord.makeReadonly(); + + logRecord.body = newBody; + logRecord.severityNumber = newSeverityNumber; + logRecord.severityText = newSeverityText; + + assert.deepStrictEqual(logRecord.body, logRecordData.body); + assert.deepStrictEqual( + logRecord.severityNumber, + logRecordData.severityNumber + ); + assert.deepStrictEqual( + logRecord.severityText, + logRecordData.severityText + ); + sinon.assert.callCount(warnStub, 3); + sinon.assert.alwaysCalledWith( + warnStub, + 'Can not execute the operation on emitted log record' + ); + warnStub.restore(); + }); + + it('should not rewrite using the set method', () => { + const warnStub = sinon.spy(diag, 'warn'); + const { logRecord } = setup(undefined, logRecordData); + logRecord.makeReadonly(); + + logRecord.setBody(newBody); + logRecord.setSeverityNumber(newSeverityNumber); + logRecord.setSeverityText(newSeverityText); + + assert.deepStrictEqual(logRecord.body, logRecordData.body); + assert.deepStrictEqual( + logRecord.severityNumber, + logRecordData.severityNumber + ); + assert.deepStrictEqual( + logRecord.severityText, + logRecordData.severityText + ); + sinon.assert.callCount(warnStub, 3); + sinon.assert.alwaysCalledWith( + warnStub, + 'Can not execute the operation on emitted log record' + ); + warnStub.restore(); + }); + }); + describe('log record processor', () => { it('should call onEmit synchronously when log record is emitted', () => { let emitted = false; diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts index f6aed30018..eb494a8dc4 100644 --- a/experimental/packages/sdk-logs/test/common/Logger.test.ts +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { Logger, LoggerProvider } from '../../src'; +import { LogRecord, Logger, LoggerProvider } from '../../src'; import { loadDefaultConfig } from '../../src/config'; const setup = () => { @@ -51,5 +51,14 @@ describe('Logger', () => { }); assert.ok(callSpy.called); }); + + it('should make log record instance readonly after emit it', () => { + const { logger } = setup(); + const makeOnlySpy = sinon.spy(LogRecord.prototype, 'makeReadonly'); + logger.emit({ + body: 'test log body', + }); + assert.ok(makeOnlySpy.called); + }); }); }); From 4cca19f753dbf5c2d7ad24012ecc13cd86a1565b Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 13:40:43 +0800 Subject: [PATCH 38/44] feat(sdk-logs): logger option support includeTraceContext & LogRecordProcessor onEmit suport context --- .../packages/api-logs/src/types/LogRecord.ts | 16 ++----- .../api-logs/src/types/LoggerOptions.ts | 6 +++ .../packages/sdk-logs/src/LogRecord.ts | 24 +++++----- .../sdk-logs/src/LogRecordProcessor.ts | 5 ++- experimental/packages/sdk-logs/src/Logger.ts | 30 ++++++++++--- .../packages/sdk-logs/src/LoggerProvider.ts | 5 ++- experimental/packages/sdk-logs/src/config.ts | 7 ++- .../src/export/ConsoleLogRecordExporter.ts | 8 ++-- .../sdk-logs/src/export/ReadableLogRecord.ts | 8 ++-- experimental/packages/sdk-logs/src/types.ts | 8 +++- .../sdk-logs/test/common/LogRecord.test.ts | 44 +++++++++++-------- .../sdk-logs/test/common/Logger.test.ts | 36 ++++++++++++--- 12 files changed, 126 insertions(+), 71 deletions(-) diff --git a/experimental/packages/api-logs/src/types/LogRecord.ts b/experimental/packages/api-logs/src/types/LogRecord.ts index d64d404752..a2c7d25ad3 100644 --- a/experimental/packages/api-logs/src/types/LogRecord.ts +++ b/experimental/packages/api-logs/src/types/LogRecord.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Attributes } from '@opentelemetry/api'; +import { Attributes, Context } from '@opentelemetry/api'; export enum SeverityNumber { UNSPECIFIED = 0, @@ -71,17 +71,7 @@ export interface LogRecord { attributes?: Attributes; /** - * 8 least significant bits are the trace flags as defined in W3C Trace Context specification. + * The Context associated with the LogRecord. */ - traceFlags?: number; - - /** - * A unique identifier for a trace. - */ - traceId?: string; - - /** - * A unique identifier for a span within a trace. - */ - spanId?: string; + context?: Context; } diff --git a/experimental/packages/api-logs/src/types/LoggerOptions.ts b/experimental/packages/api-logs/src/types/LoggerOptions.ts index 9a1c6e7c0c..a57d44a739 100644 --- a/experimental/packages/api-logs/src/types/LoggerOptions.ts +++ b/experimental/packages/api-logs/src/types/LoggerOptions.ts @@ -27,4 +27,10 @@ export interface LoggerOptions { * The instrumentation scope attributes to associate with emitted telemetry */ scopeAttributes?: Attributes; + + /** + * Specifies whether the Trace Context should automatically be passed on to the LogRecords emitted by the Logger. + * @default true + */ + includeTraceContext?: boolean; } diff --git a/experimental/packages/sdk-logs/src/LogRecord.ts b/experimental/packages/sdk-logs/src/LogRecord.ts index a611a4445b..a4601af6fc 100644 --- a/experimental/packages/sdk-logs/src/LogRecord.ts +++ b/experimental/packages/sdk-logs/src/LogRecord.ts @@ -28,17 +28,16 @@ import type { ReadableLogRecord } from './export/ReadableLogRecord'; import type { LogRecordLimits } from './types'; import { Logger } from './Logger'; -export class LogRecord implements logsAPI.LogRecord, ReadableLogRecord { - readonly time: api.HrTime; - readonly traceId?: string; - readonly spanId?: string; - readonly traceFlags?: number; +export class LogRecord implements ReadableLogRecord { + readonly hrTime: api.HrTime; + readonly spanContext?: api.SpanContext; readonly resource: IResource; readonly instrumentationScope: InstrumentationScope; readonly attributes: Attributes = {}; private _severityText?: string; private _severityNumber?: logsAPI.SeverityNumber; private _body?: string; + private _isReadonly: boolean = false; private readonly _logRecordLimits: LogRecordLimits; @@ -79,15 +78,16 @@ export class LogRecord implements logsAPI.LogRecord, ReadableLogRecord { severityText, body, attributes = {}, - spanId, - traceFlags, - traceId, + context, } = logRecord; - this.time = timeInputToHrTime(timestamp); - this.spanId = spanId; - this.traceId = traceId; - this.traceFlags = traceFlags; + this.hrTime = timeInputToHrTime(timestamp); + if (context) { + const spanContext = api.trace.getSpanContext(context); + if (spanContext && api.isSpanContextValid(spanContext)) { + this.spanContext = spanContext; + } + } this.severityNumber = severityNumber; this.severityText = severityText; this.body = body; diff --git a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts index efe075af05..c68a26c907 100644 --- a/experimental/packages/sdk-logs/src/LogRecordProcessor.ts +++ b/experimental/packages/sdk-logs/src/LogRecordProcessor.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Context } from '@opentelemetry/api'; + import { LogRecord } from './LogRecord'; export interface LogRecordProcessor { @@ -25,8 +27,9 @@ export interface LogRecordProcessor { /** * Called when a {@link LogRecord} is emit * @param logRecord the ReadWriteLogRecord that just emitted. + * @param context the current Context, or an empty Context if the Logger was obtained with include_trace_context=false */ - onEmit(logRecord: LogRecord): void; + onEmit(logRecord: LogRecord, context?: Context): void; /** * Shuts down the processor. Called when SDK is shut down. This is an diff --git a/experimental/packages/sdk-logs/src/Logger.ts b/experimental/packages/sdk-logs/src/Logger.ts index 7859309508..0e6cfbe2d9 100644 --- a/experimental/packages/sdk-logs/src/Logger.ts +++ b/experimental/packages/sdk-logs/src/Logger.ts @@ -17,6 +17,7 @@ import type * as logsAPI from '@opentelemetry/api-logs'; import type { IResource } from '@opentelemetry/resources'; import type { InstrumentationScope } from '@opentelemetry/core'; +import { context } from '@opentelemetry/api'; import type { LoggerConfig, LogRecordLimits } from './types'; import { LogRecord } from './LogRecord'; @@ -26,21 +27,38 @@ import { LogRecordProcessor } from './LogRecordProcessor'; export class Logger implements logsAPI.Logger { public readonly resource: IResource; - private readonly _logRecordLimits: LogRecordLimits; + private readonly _loggerConfig: Required; constructor( public readonly instrumentationScope: InstrumentationScope, config: LoggerConfig, private _loggerProvider: LoggerProvider ) { - const localConfig = mergeConfig(config); + this._loggerConfig = mergeConfig(config); this.resource = _loggerProvider.resource; - this._logRecordLimits = localConfig.logRecordLimits!; } public emit(logRecord: logsAPI.LogRecord): void { - const logRecordInstance = new LogRecord(this, logRecord); - this.getActiveLogRecordProcessor().onEmit(logRecordInstance); + const currentContext = this._loggerConfig.includeTraceContext + ? context.active() + : undefined; + /** + * If a Logger was obtained with include_trace_context=true, + * the LogRecords it emits MUST automatically include the Trace Context from the active Context, + * if Context has not been explicitly set. + */ + const logRecordInstance = new LogRecord(this, { + context: currentContext, + ...logRecord, + }); + /** + * the explicitly passed Context, + * the current Context, or an empty Context if the Logger was obtained with include_trace_context=false + */ + this.getActiveLogRecordProcessor().onEmit( + logRecordInstance, + currentContext + ); /** * A LogRecordProcessor may freely modify logRecord for the duration of the OnEmit call. * If logRecord is needed after OnEmit returns (i.e. for asynchronous processing) only reads are permitted. @@ -49,7 +67,7 @@ export class Logger implements logsAPI.Logger { } public getLogRecordLimits(): LogRecordLimits { - return this._logRecordLimits; + return this._loggerConfig.logRecordLimits; } public getActiveLogRecordProcessor(): LogRecordProcessor { diff --git a/experimental/packages/sdk-logs/src/LoggerProvider.ts b/experimental/packages/sdk-logs/src/LoggerProvider.ts index b2d45b5716..a51438408a 100644 --- a/experimental/packages/sdk-logs/src/LoggerProvider.ts +++ b/experimental/packages/sdk-logs/src/LoggerProvider.ts @@ -82,7 +82,10 @@ export class LoggerProvider implements logsAPI.LoggerProvider { key, new Logger( { name: loggerName, version, schemaUrl: options?.schemaUrl }, - this._config, + { + logRecordLimits: this._config.logRecordLimits, + includeTraceContext: options?.includeTraceContext, + }, this ) ); diff --git a/experimental/packages/sdk-logs/src/config.ts b/experimental/packages/sdk-logs/src/config.ts index b74ea4c5c0..af908f1650 100644 --- a/experimental/packages/sdk-logs/src/config.ts +++ b/experimental/packages/sdk-logs/src/config.ts @@ -20,7 +20,7 @@ import { getEnv, getEnvWithoutDefaults, } from '@opentelemetry/core'; -import { LoggerConfig, LogRecordLimits } from './types'; +import { LoggerConfig } from './types'; export function loadDefaultConfig() { return { @@ -30,6 +30,7 @@ export function loadDefaultConfig() { getEnv().OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT, attributeCountLimit: getEnv().OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT, }, + includeTraceContext: true, }; } @@ -68,9 +69,7 @@ export function reconfigureLimits(userConfig: LoggerConfig): LoggerConfig { * Function to merge Default configuration (as specified in './config') with * user provided configurations. */ -export function mergeConfig(userConfig: LoggerConfig): LoggerConfig & { - logRecordLimits: LogRecordLimits; -} { +export function mergeConfig(userConfig: LoggerConfig): Required { const DEFAULT_CONFIG = loadDefaultConfig(); const target = Object.assign({}, DEFAULT_CONFIG, userConfig); diff --git a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts index cfdbc23a6a..edf7c0bf8c 100644 --- a/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts +++ b/experimental/packages/sdk-logs/src/export/ConsoleLogRecordExporter.ts @@ -52,10 +52,10 @@ export class ConsoleLogRecordExporter implements LogRecordExporter { */ private _exportInfo(logRecord: ReadableLogRecord) { return { - timestamp: hrTimeToMicroseconds(logRecord.time), - traceId: logRecord.traceId, - spanId: logRecord.spanId, - traceFlags: logRecord.traceFlags, + timestamp: hrTimeToMicroseconds(logRecord.hrTime), + traceId: logRecord.spanContext?.traceId, + spanId: logRecord.spanContext?.spanId, + traceFlags: logRecord.spanContext?.traceFlags, severityText: logRecord.severityText, severityNumber: logRecord.severityNumber, body: logRecord.body, diff --git a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts index 99f18b82f9..e02d53dae9 100644 --- a/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts +++ b/experimental/packages/sdk-logs/src/export/ReadableLogRecord.ts @@ -15,15 +15,13 @@ */ import type { IResource } from '@opentelemetry/resources'; -import type { Attributes, HrTime } from '@opentelemetry/api'; +import type { Attributes, HrTime, SpanContext } from '@opentelemetry/api'; import type { InstrumentationScope } from '@opentelemetry/core'; import type { SeverityNumber } from '@opentelemetry/api-logs'; export interface ReadableLogRecord { - readonly time: HrTime; - readonly traceId?: string; - readonly spanId?: string; - readonly traceFlags?: number; + readonly hrTime: HrTime; + readonly spanContext?: SpanContext; readonly severityText?: string; readonly severityNumber?: SeverityNumber; readonly body?: string; diff --git a/experimental/packages/sdk-logs/src/types.ts b/experimental/packages/sdk-logs/src/types.ts index f6e1abc436..5bdad2a1e0 100644 --- a/experimental/packages/sdk-logs/src/types.ts +++ b/experimental/packages/sdk-logs/src/types.ts @@ -16,7 +16,7 @@ import type { IResource } from '@opentelemetry/resources'; -export interface LoggerProviderConfig extends LoggerConfig { +export interface LoggerProviderConfig { /** Resource associated with trace telemetry */ resource?: IResource; @@ -25,11 +25,17 @@ export interface LoggerProviderConfig extends LoggerConfig { * The default value is 30000ms */ forceFlushTimeoutMillis?: number; + + /** Log Record Limits*/ + logRecordLimits?: LogRecordLimits; } export interface LoggerConfig { /** Log Record Limits*/ logRecordLimits?: LogRecordLimits; + + /** include Trace Context */ + includeTraceContext?: boolean; } export interface LogRecordLimits { diff --git a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts index 3e1a2c6a08..cc7ce8bafd 100644 --- a/experimental/packages/sdk-logs/test/common/LogRecord.test.ts +++ b/experimental/packages/sdk-logs/test/common/LogRecord.test.ts @@ -16,7 +16,14 @@ import * as sinon from 'sinon'; import * as assert from 'assert'; -import { Attributes, AttributeValue, diag } from '@opentelemetry/api'; +import { + Attributes, + AttributeValue, + diag, + ROOT_CONTEXT, + trace, + TraceFlags, +} from '@opentelemetry/api'; import * as logsAPI from '@opentelemetry/api-logs'; import type { HrTime } from '@opentelemetry/api'; import { hrTimeToMilliseconds, timeInputToHrTime } from '@opentelemetry/core'; @@ -61,14 +68,21 @@ describe('LogRecord', () => { it('should have a default timestamp', () => { const { logRecord } = setup(); - assert.ok(logRecord.time !== undefined); + assert.ok(logRecord.hrTime !== undefined); assert.ok( - hrTimeToMilliseconds(logRecord.time) > + hrTimeToMilliseconds(logRecord.hrTime) > hrTimeToMilliseconds(performanceTimeOrigin) ); }); it('should return LogRecord', () => { + const spanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + const activeContext = trace.setSpanContext(ROOT_CONTEXT, spanContext); + const logRecordData: logsAPI.LogRecord = { timestamp: new Date().getTime(), severityNumber: logsAPI.SeverityNumber.DEBUG, @@ -77,16 +91,14 @@ describe('LogRecord', () => { attributes: { name: 'test name', }, - traceId: 'trance id', - spanId: 'span id', - traceFlags: 1, + context: activeContext, }; const { logRecord, resource, instrumentationScope } = setup( undefined, logRecordData ); assert.deepStrictEqual( - logRecord.time, + logRecord.hrTime, timeInputToHrTime(logRecordData.timestamp!) ); assert.strictEqual( @@ -96,9 +108,12 @@ describe('LogRecord', () => { assert.strictEqual(logRecord.severityText, logRecordData.severityText); assert.strictEqual(logRecord.body, logRecordData.body); assert.deepStrictEqual(logRecord.attributes, logRecordData.attributes); - assert.deepStrictEqual(logRecord.traceId, logRecordData.traceId); - assert.deepStrictEqual(logRecord.spanId, logRecordData.spanId); - assert.deepStrictEqual(logRecord.traceFlags, logRecordData.traceFlags); + assert.strictEqual(logRecord.spanContext?.traceId, spanContext.traceId); + assert.strictEqual(logRecord.spanContext?.spanId, spanContext.spanId); + assert.strictEqual( + logRecord.spanContext?.traceFlags, + spanContext.traceFlags + ); assert.deepStrictEqual(logRecord.resource, resource); assert.deepStrictEqual( logRecord.instrumentationScope, @@ -115,9 +130,6 @@ describe('LogRecord', () => { attributes: { name: 'test name', }, - traceId: 'trance id', - spanId: 'span id', - traceFlags: 1, }; const { logRecord } = setup(undefined, logRecordData); @@ -258,9 +270,6 @@ describe('LogRecord', () => { attributes: { name: 'test name', }, - traceId: 'trance id', - spanId: 'span id', - traceFlags: 1, }; const newBody = 'this is a new body'; @@ -302,9 +311,6 @@ describe('LogRecord', () => { attributes: { name: 'test name', }, - traceId: 'trance id', - spanId: 'span id', - traceFlags: 1, }; const newBody = 'this is a new body'; diff --git a/experimental/packages/sdk-logs/test/common/Logger.test.ts b/experimental/packages/sdk-logs/test/common/Logger.test.ts index eb494a8dc4..5df756c901 100644 --- a/experimental/packages/sdk-logs/test/common/Logger.test.ts +++ b/experimental/packages/sdk-logs/test/common/Logger.test.ts @@ -17,19 +17,19 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { LogRecord, Logger, LoggerProvider } from '../../src'; +import { LogRecord, Logger, LoggerConfig, LoggerProvider } from '../../src'; import { loadDefaultConfig } from '../../src/config'; +import { context } from '@opentelemetry/api'; -const setup = () => { - const { forceFlushTimeoutMillis, logRecordLimits } = loadDefaultConfig(); +const setup = (loggerConfig: LoggerConfig = {}) => { const logger = new Logger( { name: 'test name', version: 'test version', schemaUrl: 'test schema url', }, - { logRecordLimits }, - new LoggerProvider({ forceFlushTimeoutMillis }) + loggerConfig, + new LoggerProvider() ); return { logger }; }; @@ -40,6 +40,14 @@ describe('Logger', () => { const { logger } = setup(); assert.ok(logger instanceof Logger); }); + + it('should a default value with config.includeTraceContext', () => { + const { logger } = setup(); + assert.ok( + logger['_loggerConfig'].includeTraceContext === + loadDefaultConfig().includeTraceContext + ); + }); }); describe('emit', () => { @@ -60,5 +68,23 @@ describe('Logger', () => { }); assert.ok(makeOnlySpy.called); }); + + it('should emit with current Context when includeTraceContext is true', () => { + const { logger } = setup({ includeTraceContext: true }); + const callSpy = sinon.spy(logger.getActiveLogRecordProcessor(), 'onEmit'); + logger.emit({ + body: 'test log body', + }); + assert.ok(callSpy.calledWith(sinon.match.any, context.active())); + }); + + it('should emit with empty Context when includeTraceContext is false', () => { + const { logger } = setup({ includeTraceContext: false }); + const callSpy = sinon.spy(logger.getActiveLogRecordProcessor(), 'onEmit'); + logger.emit({ + body: 'test log body', + }); + assert.ok(callSpy.calledWith(sinon.match.any, undefined)); + }); }); }); From 4cb03370b0dcb1e4b4d1fb6baa862661721252c2 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 14:09:26 +0800 Subject: [PATCH 39/44] feat(sdk-logs): update version --- CHANGELOG.md | 1 + experimental/CHANGELOG.md | 2 +- experimental/packages/sdk-logs/package.json | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e894e1efd2..a91becc878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ For experimental package changes, see the [experimental CHANGELOG](experimental/ ### :rocket: (Enhancement) * feat(tracing): log span name and IDs when span end is called multiple times [#3716](https://github.com/open-telemetry/opentelemetry-js/pull/3716) +* feat(core): add logs environment variables; add timeout utils method. [#3549](https://github.com/open-telemetry/opentelemetry-js/pull/3549/) @fuaiyi ### :bug: (Bug Fix) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 37e5310057..f96b037cdb 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -8,8 +8,8 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) +* feat(api-logs): 1.`LogRecord` fields update: `traceFlags`/`traceId`/`spanId` -> `context`; 2.`Logger` supports configuring `includeTraceContext`; 3.The `onEmit` method of `LogRecordProcessor` supports the `context` field. [#3549](https://github.com/open-telemetry/opentelemetry-js/pull/3549/) @fuaiyi * feat(sdk-logs): logs sdk implementation. [#3549](https://github.com/open-telemetry/opentelemetry-js/pull/3549/) @fuaiyi -* feat(core): add logs environment variables; add timeout utils method. [#3549](https://github.com/open-telemetry/opentelemetry-js/pull/3549/) @fuaiyi ### :bug: (Bug Fix) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 5314b010be..3bea1e5276 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -1,6 +1,6 @@ { "name": "@opentelemetry/sdk-logs", - "version": "0.36.1", + "version": "0.37.0", "publishConfig": { "access": "public" }, @@ -68,7 +68,6 @@ ], "sideEffects": false, "devDependencies": { - "@opentelemetry/api": "^1.4.1", "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", @@ -90,8 +89,9 @@ "@opentelemetry/api": "^1.4.1" }, "dependencies": { - "@opentelemetry/core": "^1.10.1", - "@opentelemetry/resources": "^1.10.1", - "@opentelemetry/api-logs": "^0.36.1" + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/core": "^1.11.0", + "@opentelemetry/resources": "^1.11.0", + "@opentelemetry/api-logs": "^0.37.0" } } From dcc1c6abb873d38e20ffddbec6cc7c2f9d22dca5 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 14:56:08 +0800 Subject: [PATCH 40/44] feat(sdk-logs): update version --- experimental/examples/logs/package.json | 4 ++-- lerna.json | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/experimental/examples/logs/package.json b/experimental/examples/logs/package.json index 3b4309c8ed..9fff224f36 100644 --- a/experimental/examples/logs/package.json +++ b/experimental/examples/logs/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.4.1", - "@opentelemetry/api-logs": "^0.36.1", - "@opentelemetry/sdk-logs": "^0.36.1" + "@opentelemetry/api-logs": "^0.37.0", + "@opentelemetry/sdk-logs": "^0.37.0" } } diff --git a/lerna.json b/lerna.json index 4f26aaf43f..6eba3f4874 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,6 @@ { "version": "independent", "npmClient": "npm", - "useNx": "fasle", "packages": [ "api", "packages/*", From 0c3e7b344fd8b254d035e128a009242568ac4590 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 15:35:44 +0800 Subject: [PATCH 41/44] feat(sdk-logs): update logs example with typescript --- experimental/examples/logs/index.js | 30 ----------------- experimental/examples/logs/index.ts | 43 ++++++++++++++++++++++++ experimental/examples/logs/package.json | 6 +++- experimental/examples/logs/tsconfig.json | 19 +++++++++++ 4 files changed, 67 insertions(+), 31 deletions(-) delete mode 100644 experimental/examples/logs/index.js create mode 100644 experimental/examples/logs/index.ts create mode 100644 experimental/examples/logs/tsconfig.json diff --git a/experimental/examples/logs/index.js b/experimental/examples/logs/index.js deleted file mode 100644 index 53d47d604c..0000000000 --- a/experimental/examples/logs/index.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -const { DiagConsoleLogger, DiagLogLevel, diag } = require('@opentelemetry/api'); -const logsAPI = require('@opentelemetry/api-logs'); -const { SeverityNumber } = require('@opentelemetry/api-logs'); -const { - LoggerProvider, - ConsoleLogRecordExporter, - SimpleLogRecordProcessor, -} = require('@opentelemetry/sdk-logs'); - -// Optional and only needed to see the internal diagnostic logging (during development) -diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); - -const loggerProvider = new LoggerProvider(); -loggerProvider.addLogRecordProcessor( - new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()) -); - -logsAPI.logs.setGlobalLoggerProvider(loggerProvider); - -const logger = logsAPI.logs.getLogger('example', '1.0.0'); - -// emit a log record -logger.emit({ - severityNumber: SeverityNumber.INFO, - severityText: 'INFO', - body: 'this is a log record body', - attributes: { 'log.type': 'LogRecord' }, -}); diff --git a/experimental/examples/logs/index.ts b/experimental/examples/logs/index.ts new file mode 100644 index 0000000000..4579f32835 --- /dev/null +++ b/experimental/examples/logs/index.ts @@ -0,0 +1,43 @@ +/* + * 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 { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api'; +import { logs, SeverityNumber } from '@opentelemetry/api-logs'; +import { + LoggerProvider, + ConsoleLogRecordExporter, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; + +// Optional and only needed to see the internal diagnostic logging (during development) +diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); + +const loggerProvider = new LoggerProvider(); +loggerProvider.addLogRecordProcessor( + new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()) +); + +logs.setGlobalLoggerProvider(loggerProvider); + +const logger = logs.getLogger('example', '1.0.0'); + +// emit a log record +logger.emit({ + severityNumber: SeverityNumber.INFO, + severityText: 'INFO', + body: 'this is a log record body', + attributes: { 'log.type': 'custom' }, +}); diff --git a/experimental/examples/logs/package.json b/experimental/examples/logs/package.json index 9fff224f36..187e55b2fb 100644 --- a/experimental/examples/logs/package.json +++ b/experimental/examples/logs/package.json @@ -3,11 +3,15 @@ "version": "0.1.0", "private": true, "scripts": { - "start": "node index.js" + "start": "ts-node index.ts" }, "dependencies": { "@opentelemetry/api": "^1.4.1", "@opentelemetry/api-logs": "^0.37.0", "@opentelemetry/sdk-logs": "^0.37.0" + }, + "devDependencies": { + "ts-node": "^10.9.1", + "@types/node": "18.6.5" } } diff --git a/experimental/examples/logs/tsconfig.json b/experimental/examples/logs/tsconfig.json new file mode 100644 index 0000000000..87efd27307 --- /dev/null +++ b/experimental/examples/logs/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "build", + "rootDir": "." + }, + "include": ["./index.ts"], + "references": [ + { + "path": "../../../api" + }, + { + "path": "../../../experimental/packages/api-logs" + }, + { + "path": "../../../experimental/packages/sdk-logs" + } + ] +} From 6d0cc64394cf6986b9d0909a50738814e8c7b424 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 15:56:28 +0800 Subject: [PATCH 42/44] feat(sdk-logs): update peerDependencies --- experimental/packages/sdk-logs/package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index 3bea1e5276..acd3eb5ab5 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -86,12 +86,11 @@ "typescript": "4.4.4" }, "peerDependencies": { - "@opentelemetry/api": "^1.4.1" + "@opentelemetry/api": ">=1.3.0 <1.5.0", + "@opentelemetry/api-logs": ">0.37.0" }, "dependencies": { - "@opentelemetry/api": "^1.4.1", "@opentelemetry/core": "^1.11.0", - "@opentelemetry/resources": "^1.11.0", - "@opentelemetry/api-logs": "^0.37.0" + "@opentelemetry/resources": "^1.11.0" } } From e01b734aaff2e58e25945ee320a322292c13d7dc Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 16:27:33 +0800 Subject: [PATCH 43/44] feat(sdk-logs): peer-api-check support @opentelemetry/api-logs --- experimental/packages/sdk-logs/package.json | 10 +++++---- scripts/peer-api-check.js | 24 +++++++++++++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index acd3eb5ab5..cbb11fc7fd 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -67,10 +67,16 @@ "README.md" ], "sideEffects": false, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.5.0", + "@opentelemetry/api-logs": ">0.37.0" + }, "devDependencies": { "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", + "@opentelemetry/api": ">=1.4.0 <1.5.0", + "@opentelemetry/api-logs": ">0.37.0", "codecov": "3.8.3", "karma": "6.3.16", "karma-chrome-launcher": "3.1.0", @@ -85,10 +91,6 @@ "ts-mocha": "10.0.0", "typescript": "4.4.4" }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.5.0", - "@opentelemetry/api-logs": ">0.37.0" - }, "dependencies": { "@opentelemetry/core": "^1.11.0", "@opentelemetry/resources": "^1.11.0" diff --git a/scripts/peer-api-check.js b/scripts/peer-api-check.js index e372af1eb2..27abe9124d 100644 --- a/scripts/peer-api-check.js +++ b/scripts/peer-api-check.js @@ -24,14 +24,24 @@ const appRoot = process.cwd(); const packageJsonUrl = path.resolve(`${appRoot}/package.json`); const pjson = require(packageJsonUrl); -if (pjson.dependencies && pjson.dependencies["@opentelemetry/api"]) - throw new Error(`Package ${pjson.name} depends on API but it should be a peer dependency`); +const needCheckPackages = ['@opentelemetry/api', '@opentelemetry/api-logs']; -const peerVersion = pjson.peerDependencies && pjson.peerDependencies["@opentelemetry/api"] -const devVersion = pjson.devDependencies && pjson.devDependencies["@opentelemetry/api"] -if (peerVersion) { +function checkPackage(package) { + if (pjson.dependencies && pjson.dependencies[package]) + throw new Error( + `Package ${pjson.name} depends on API but it should be a peer dependency` + ); + + const peerVersion = pjson.peerDependencies && pjson.peerDependencies[package]; + const devVersion = pjson.devDependencies && pjson.devDependencies[package]; + if (peerVersion) { if (!semver.subset(devVersion, peerVersion)) { - throw new Error(`Package ${pjson.name} depends on peer API version ${peerVersion} but version ${devVersion} in development`); + throw new Error( + `Package ${pjson.name} depends on peer API version ${peerVersion} but version ${devVersion} in development` + ); } console.log(`${pjson.name} OK`); -} \ No newline at end of file + } +} + +needCheckPackages.forEach(checkPackage); From d8d8a95cca6aff4655716c5d587deb50612b2069 Mon Sep 17 00:00:00 2001 From: fugao Date: Wed, 12 Apr 2023 16:37:59 +0800 Subject: [PATCH 44/44] feat(sdk-logs): update peerDependencies --- experimental/packages/sdk-logs/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experimental/packages/sdk-logs/package.json b/experimental/packages/sdk-logs/package.json index cbb11fc7fd..fe988951eb 100644 --- a/experimental/packages/sdk-logs/package.json +++ b/experimental/packages/sdk-logs/package.json @@ -69,14 +69,14 @@ "sideEffects": false, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.5.0", - "@opentelemetry/api-logs": ">0.37.0" + "@opentelemetry/api-logs": ">=0.37.0" }, "devDependencies": { "@types/mocha": "10.0.0", "@types/node": "18.6.5", "@types/sinon": "10.0.13", "@opentelemetry/api": ">=1.4.0 <1.5.0", - "@opentelemetry/api-logs": ">0.37.0", + "@opentelemetry/api-logs": ">=0.37.0", "codecov": "3.8.3", "karma": "6.3.16", "karma-chrome-launcher": "3.1.0",