diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..3cca8717e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +protos diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 4ada5863d..d3bf0dbf8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,5 @@ { "gax": "4.5.0", - "tools": "0.4.6" + "tools": "0.4.6", + "logging-utils": "0.0.0" } diff --git a/logging-utils/.eslintignore b/logging-utils/.eslintignore new file mode 100644 index 000000000..ea5b04aeb --- /dev/null +++ b/logging-utils/.eslintignore @@ -0,0 +1,7 @@ +**/node_modules +**/coverage +test/fixtures +build/ +docs/ +protos/ +samples/generated/ diff --git a/logging-utils/.eslintrc.json b/logging-utils/.eslintrc.json new file mode 100644 index 000000000..3e8d97ccb --- /dev/null +++ b/logging-utils/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": "./node_modules/gts", + "root": true +} diff --git a/logging-utils/.gitignore b/logging-utils/.gitignore new file mode 100644 index 000000000..5fc9f52a6 --- /dev/null +++ b/logging-utils/.gitignore @@ -0,0 +1,24 @@ +coverage +npm-debug.log +**/*.log +**/node_modules +.coverage +.nyc_output +docs/ +protos/google/ +out/ +system-test/secrets.js +system-test/*key.json +build +.vscode +package-lock.json +.system-test-run/ +.kitchen-sink/ +.showcase-server-dir/ +.compileProtos-test/ +.minify-test/ +__pycache__ +doc/ +dist/ +*.tgz +**/*.tgz diff --git a/logging-utils/.prettierignore b/logging-utils/.prettierignore new file mode 100644 index 000000000..9340ad9b8 --- /dev/null +++ b/logging-utils/.prettierignore @@ -0,0 +1,6 @@ +**/node_modules +**/coverage +test/fixtures +build/ +docs/ +protos/ diff --git a/logging-utils/.prettierrc.js b/logging-utils/.prettierrc.js new file mode 100644 index 000000000..120c6aa3e --- /dev/null +++ b/logging-utils/.prettierrc.js @@ -0,0 +1,17 @@ +// Copyright 2024 Google LLC +// +// 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. + +module.exports = { + ...require('gts/.prettierrc.json') +} diff --git a/logging-utils/CHANGELOG.md b/logging-utils/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/logging-utils/CODE_OF_CONDUCT.md b/logging-utils/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..2add2547a --- /dev/null +++ b/logging-utils/CODE_OF_CONDUCT.md @@ -0,0 +1,94 @@ + +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of +experience, education, socio-economic status, nationality, personal appearance, +race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +This Code of Conduct also applies outside the project spaces when the Project +Steward has a reasonable belief that an individual's behavior may have a +negative impact on the project or its community. + +## Conflict Resolution + +We do not believe that all conflict is bad; healthy debate and disagreement +often yield positive results. However, it is never okay to be disrespectful or +to engage in behavior that violates the project’s code of conduct. + +If you see someone violating the code of conduct, you are encouraged to address +the behavior directly with those involved. Many issues can be resolved quickly +and easily, and this gives people more control over the outcome of their +dispute. If you are unable to resolve the matter for any reason, or if the +behavior is threatening or harassing, report it. We are dedicated to providing +an environment where participants feel welcome and safe. + +Reports should be directed to *googleapis-stewards@google.com*, the +Project Steward(s) for *Google Cloud Client Libraries*. It is the Project Steward’s duty to +receive and address reported violations of the code of conduct. They will then +work with a committee consisting of representatives from the Open Source +Programs Office and the Google Open Source Strategy team. If for any reason you +are uncomfortable reaching out to the Project Steward, please email +opensource@google.com. + +We will investigate every complaint, but you may not receive a direct response. +We will use our discretion in determining when and how to follow up on reported +incidents, which may range from not taking action to permanent expulsion from +the project and project-sponsored spaces. We will notify the accused of the +report and provide them an opportunity to discuss it before any action is taken. +The identity of the reporter will be omitted from the details of the report +supplied to the accused. In potentially harmful situations, such as ongoing +harassment or threats to anyone's safety, we may take action without notice. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, +available at +https://www.contributor-covenant.org/version/1/4/code-of-conduct.html \ No newline at end of file diff --git a/logging-utils/CONTRIBUTING.md b/logging-utils/CONTRIBUTING.md new file mode 100644 index 000000000..72c44cada --- /dev/null +++ b/logging-utils/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# How to become a contributor and submit your own code + +**Table of contents** + +* [Contributor License Agreements](#contributor-license-agreements) +* [Contributing a patch](#contributing-a-patch) +* [Running the tests](#running-the-tests) +* [Releasing the library](#releasing-the-library) + +## Contributor License Agreements + +We'd love to accept your sample apps and patches! Before we can take them, we +have to jump a couple of legal hurdles. + +Please fill out either the individual or corporate Contributor License Agreement +(CLA). + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). + +Follow either of the two links above to access the appropriate CLA and +instructions for how to sign and return it. Once we receive it, we'll be able to +accept your pull requests. + +## Contributing A Patch + +1. Submit an issue describing your proposed change to the repo in question. +1. The repo owner will respond to your issue promptly. +1. If your proposed change is accepted, and you haven't already done so, sign a + Contributor License Agreement (see details above). +1. Fork the desired repo, develop and test your code changes. +1. Ensure that your code adheres to the existing style in the code to which + you are contributing. +1. Ensure that your code has an appropriate set of tests which all pass. +1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. +1. Submit a pull request. + +### Before you begin + +1. [Select or create a Cloud Platform project][projects]. +1. [Set up authentication with a service account][auth] so you can access the + API from your local workstation. + + +## Running the tests + +1. [Prepare your environment for Node.js setup][setup]. + +1. Install dependencies: + + npm install + +1. Run the tests: + + # Run unit tests. + npm test + + # Run sample integration tests. + npm run samples-test + + # Run all system tests. + npm run system-test + +1. Lint (and maybe fix) any changes: + + npm run fix + +[setup]: https://cloud.google.com/nodejs/docs/setup +[projects]: https://console.cloud.google.com/project +[billing]: https://support.google.com/cloud/answer/6293499#enable-billing + +[auth]: https://cloud.google.com/docs/authentication/getting-started \ No newline at end of file diff --git a/logging-utils/LICENSE b/logging-utils/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/logging-utils/LICENSE @@ -0,0 +1,202 @@ + + 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/logging-utils/package.json b/logging-utils/package.json new file mode 100644 index 000000000..1c06ace0f --- /dev/null +++ b/logging-utils/package.json @@ -0,0 +1,39 @@ +{ + "name": "google-logging-utils", + "version": "0.0.3", + "description": "A debug logger package for other Google libraries", + "main": "build/src/index.js", + "files": [ + "build/src" + ], + "scripts": { + "pretest": "npm run prepare", + "test": "c8 mocha build/test", + "lint": "gts check test src samples", + "clean": "gts clean", + "compile": "tsc -p . && cp -r test/fixtures build/test", + "fix": "gts fix", + "prepare": "npm run compile", + "precompile": "gts clean", + "samples-test": "echo no samples 🙀", + "system-test": "echo no system tests 🙀" + }, + "author": "Google API Authors", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/googleapis/gax-nodejs.git", + "directory": "logging-utils" + }, + "devDependencies": { + "@types/mocha": "^10.0.10", + "@types/node": "^22.9.1", + "c8": "^9.0.0", + "gts": "^5.0.0", + "mocha": "^9.0.0", + "typescript": "^5.1.6" + }, + "engines": { + "node": ">=14" + } +} diff --git a/logging-utils/samples/javascript/quickstart.js b/logging-utils/samples/javascript/quickstart.js new file mode 100644 index 000000000..e158d9461 --- /dev/null +++ b/logging-utils/samples/javascript/quickstart.js @@ -0,0 +1,34 @@ +// Copyright 2024 Google LLC +// +// 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. + +// This is a generated sample, using the typeless sample bot. Please +// look for the source TypeScript sample (.ts) for modifications. +'use strict'; + +// sample-metadata: +// title: Quickstart +// description: A quick introduction to using the Pub/Sub client library. +// usage: node quickstart.js + +// [!START logging_utils_quickstart] +const {log} = require('google-logging-utils'); + +function main() { + const test = log('testing'); + test({other: {foo: 'bar'}}, 'boo'); + test.info('info'); +} +// [!END logging_utils_quickstart] + +main(); diff --git a/logging-utils/samples/package.json b/logging-utils/samples/package.json new file mode 100644 index 000000000..5715d4fe6 --- /dev/null +++ b/logging-utils/samples/package.json @@ -0,0 +1,37 @@ +{ + "name": "google-logging-samples", + "version": "0.0.1", + "description": "Samples for debug logger", + "main": "index.js", + "scripts": { + "test": "c8 mocha build/system-test", + "pretest": "npm run compile", + "compile": "tsc -p .", + "typeless": "npx typeless-sample-bot --outputpath javascript --targets typescript --recursive", + "posttypeless": "npx eslint --fix javascript", + "preinstall": "npm link ../" + }, + "files": [ + "javascript/*.js", + "typescript/*.ts" + ], + "private": true, + "author": "Google LLC", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/googleapis/gax-nodejs.git", + "directory": "logging-utils/samples" + }, + "engines": { + "node": ">=14" + }, + "dependencies": { + "google-logging-utils": "0.0.2" + }, + "devDependencies": { + "@google-cloud/typeless-sample-bot": "^2.1.0", + "gts": "^5.0.0", + "mocha": "^9.0.0" + } +} diff --git a/logging-utils/samples/system-test/test.quickstart.ts b/logging-utils/samples/system-test/test.quickstart.ts new file mode 100644 index 000000000..37edb0abe --- /dev/null +++ b/logging-utils/samples/system-test/test.quickstart.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2024 Google LLC + * + * 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. + */ + +import * as path from 'path'; +import * as assert from 'assert'; +import {describe, it} from 'mocha'; +import * as cp from 'child_process'; + +const cwd = path.join(__dirname, '../..'); + +describe('Quickstart', () => { + it('should run quickstart sample', async () => { + // Try first without the environment variable. + const stdout1 = cp.execSync('node javascript/quickstart.js 2>&1', { + cwd, + encoding: 'utf-8', + }); + assert(!/testing.*DEFAULT.*other.*foo.*bar/.test(stdout1)); + assert(!/testing.*INFO.*info/.test(stdout1)); + + const stdout2 = cp.execSync('node javascript/quickstart.js 2>&1', { + cwd, + encoding: 'utf-8', + env: Object.assign( + { + GOOGLE_SDK_NODE_LOGGING: 'all', + }, + process.env + ), + }); + assert(/testing.*DEFAULT.*other.*foo.*bar/.test(stdout2)); + assert(/testing.*INFO.*info/.test(stdout2)); + }); +}); diff --git a/logging-utils/samples/tsconfig.json b/logging-utils/samples/tsconfig.json new file mode 100644 index 000000000..5f890add8 --- /dev/null +++ b/logging-utils/samples/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build", + "resolveJsonModule": true, + "lib": [ + "es2018", + "dom" + ], + "moduleResolution": "node" + }, + "include": [ + "system-test/*.ts" + ], + "exclude": [ + "typescript/*.ts" + ] +} diff --git a/logging-utils/samples/typescript/quickstart.ts b/logging-utils/samples/typescript/quickstart.ts new file mode 100644 index 000000000..c08bc1860 --- /dev/null +++ b/logging-utils/samples/typescript/quickstart.ts @@ -0,0 +1,30 @@ +// Copyright 2024 Google LLC +// +// 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. + +// sample-metadata: +// title: Quickstart +// description: A quick introduction to using the Pub/Sub client library. +// usage: node quickstart.js + +// [!START logging_utils_quickstart] +import {log} from 'google-logging-utils'; + +function main() { + const test = log('testing'); + test({other: {foo: 'bar'}}, 'boo'); + test.info('info'); +} +// [!END logging_utils_quickstart] + +main(); diff --git a/logging-utils/src/colours.ts b/logging-utils/src/colours.ts new file mode 100644 index 000000000..bd31ae85f --- /dev/null +++ b/logging-utils/src/colours.ts @@ -0,0 +1,89 @@ +// Copyright 2024 Google LLC +// +// 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. + +// This implements a very limited version of Node's internal util/colors.js. +// We do this to get the functionality for logging without needing to pull +// in third party dependencies. +// +// https://github.com/nodejs/node/blob/73414f34e8f3e1f4dac3bb35e137c4030afb4267/lib/internal/util/colors.js#L9 + +import * as tty from 'tty'; + +/** + * Handles figuring out if we can use ANSI colours and handing out the escape codes. + * + * This is for package-internal use only, and may change at any time. + * + * @private + * @internal + */ +export class Colours { + static enabled = false; + static reset = ''; + static bright = ''; + static dim = ''; + + static red = ''; + static green = ''; + static yellow = ''; + static blue = ''; + static magenta = ''; + static cyan = ''; + static white = ''; + static grey = ''; + + /** + * @param stream The stream (e.g. process.stderr) + * @returns true if the stream should have colourization enabled + */ + static isEnabled(stream: tty.WriteStream): boolean { + return ( + stream.isTTY && + (typeof stream.getColorDepth === 'function' + ? stream.getColorDepth() > 2 + : true) + ); + } + + static refresh(): void { + Colours.enabled = Colours.isEnabled(process.stderr); + if (!this.enabled) { + Colours.reset = ''; + Colours.bright = ''; + Colours.dim = ''; + Colours.red = ''; + Colours.green = ''; + Colours.yellow = ''; + Colours.blue = ''; + Colours.magenta = ''; + Colours.cyan = ''; + Colours.white = ''; + Colours.grey = ''; + } else { + Colours.reset = '\u001b[0m'; + Colours.bright = '\u001b[1m'; + Colours.dim = '\u001b[2m'; + Colours.red = '\u001b[31m'; + Colours.green = '\u001b[32m'; + Colours.yellow = '\u001b[33m'; + Colours.blue = '\u001b[34m'; + Colours.magenta = '\u001b[35m'; + Colours.cyan = '\u001b[36m'; + Colours.white = '\u001b[37m'; + Colours.grey = '\u001b[90m'; + } + } +} + +Colours.refresh(); diff --git a/logging-utils/src/index.ts b/logging-utils/src/index.ts new file mode 100644 index 000000000..7bc6c3273 --- /dev/null +++ b/logging-utils/src/index.ts @@ -0,0 +1,15 @@ +// Copyright 2024 Google LLC +// +// 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 './logging-utils'; diff --git a/logging-utils/src/logging-utils.ts b/logging-utils/src/logging-utils.ts new file mode 100644 index 000000000..b36d36203 --- /dev/null +++ b/logging-utils/src/logging-utils.ts @@ -0,0 +1,565 @@ +// Copyright 2021-2024 Google LLC +// +// 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 {EventEmitter} from 'events'; +import * as process from 'process'; +import * as util from 'util'; +import {Colours} from './colours'; + +// Some functions (as noted) are based on the Node standard library, from +// the following file: +// +// https://github.com/nodejs/node/blob/main/lib/internal/util/debuglog.js + +/** + * This module defines an ad-hoc debug logger for Google Cloud Platform + * client libraries in Node. An ad-hoc debug logger is a tool which lets + * users use an external, unified interface (in this case, environment + * variables) to determine what logging they want to see at runtime. This + * isn't necessarily fed into the console, but is meant to be under the + * control of the user. The kind of logging that will be produced by this + * is more like "call retry happened", not "events you'd want to record + * in Cloud Logger". + * + * More for Googlers implementing libraries with it: + * go/cloud-client-logging-design + */ + +/** + * Possible log levels. These are a subset of Cloud Observability levels. + * https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity + */ +export enum LogSeverity { + DEFAULT = 'DEFAULT', + DEBUG = 'DEBUG', + INFO = 'INFO', + WARNING = 'WARNING', + ERROR = 'ERROR', +} + +/** + * A set of suggested log metadata fields. + */ +export interface LogFields { + /** + * Log level - undefined/null === DEFAULT. + */ + severity?: LogSeverity; + + /** + * If this log is associated with an OpenTelemetry trace, you can put the + * trace ID here to pass on that association. + */ + telemetryTraceId?: string; + + /** + * If this log is associated with an OpenTelemetry trace, you can put the + * span ID here to pass on that association. + */ + telemetrySpanId?: string; + + /** + * This is a catch-all for any other items you might want to go into + * structured logs. Library implementers, please see the spec docs above + * for the items envisioned to go here. + */ + other?: unknown; +} + +/** + * Adds typings for event sinks. + */ +export declare interface AdhocDebugLogger { + on( + event: 'log', + listener: (fields: LogFields, args: unknown[]) => void + ): this; + on(event: string, listener: Function): this; +} + +/** + * Our logger instance. This actually contains the meat of dealing + * with log lines, including EventEmitter. This contains the function + * that will be passed back to users of the package. + */ +export class AdhocDebugLogger extends EventEmitter { + // Our namespace (system/subsystem/etc) + namespace: string; + + // The function we'll call with new log lines. + // Should be built in Node util stuff, or the "debug" package, or whatever. + upstream: AdhocDebugLogCallable; + + // Self-referential function wrapper that calls invoke() on us. + func: AdhocDebugLogFunction; + + /** + * @param upstream The backend will pass a function that will be + * called whenever our logger function is invoked. + */ + constructor(namespace: string, upstream: AdhocDebugLogCallable) { + super(); + + this.namespace = namespace; + this.upstream = upstream; + this.func = Object.assign(this.invoke.bind(this), { + // Also add an instance pointer back to us. + instance: this, + + // And pull over the EventEmitter functionality. + on: (event: string, listener: (args: unknown[]) => void) => + this.on(event, listener), + }) as unknown as AdhocDebugLogFunction; + + // Convenience methods for log levels. + this.func.debug = (...args) => + this.invokeSeverity(LogSeverity.DEBUG, ...args); + this.func.info = (...args) => + this.invokeSeverity(LogSeverity.INFO, ...args); + this.func.warn = (...args) => + this.invokeSeverity(LogSeverity.WARNING, ...args); + this.func.error = (...args) => + this.invokeSeverity(LogSeverity.ERROR, ...args); + this.func.sublog = (namespace: string) => log(namespace, this.func); + } + + invoke(fields: LogFields, ...args: unknown[]): void { + // Push out any upstream logger first. + if (this.upstream) { + this.upstream(fields, ...args); + } + + // Emit sink events. + this.emit('log', fields, args); + } + + invokeSeverity(severity: LogSeverity, ...args: unknown[]): void { + this.invoke({severity}, ...args); + } +} + +/** + * This can be used in place of a real logger while waiting for Promises or disabling logging. + */ +export const placeholder = new AdhocDebugLogger('', () => {}).func; + +/** + * When the user receives a log function (below), this will be the basic function + * call interface for it. + */ +export interface AdhocDebugLogCallable { + (fields: LogFields, ...args: unknown[]): void; +} + +/** + * Adds typing info for the EventEmitter we're adding to the returned function. + * + * Note that this interface may change at any time, as we're reserving the + * right to add new backends at the logger level. + * + * @private + * @internal + */ +export interface AdhocDebugLogFunction extends AdhocDebugLogCallable { + instance: AdhocDebugLogger; + on( + event: 'log', + listener: (fields: LogFields, args: unknown[]) => void + ): this; + + debug(...args: unknown[]): void; + info(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; + sublog(namespace: string): AdhocDebugLogFunction; +} + +/** + * One of these can be passed to support a third-party backend, like "debug". + * We're splitting this out because ESM can complicate optional module loading. + * + * Note that this interface may change at any time, as we're reserving the + * right to add new backends at the logger level. + * + * @private + * @internal + */ +export interface DebugLogBackend { + /** + * Outputs a log to this backend. + * + * @param namespace The "system" that will be used for filtering. This may also + * include a "subsystem" in the form "system:subsystem". + * @param fields Logging fields to be included as metadata. + * @param args Any parameters to passed to a utils.format() type formatter. + */ + log(namespace: string, fields: LogFields, ...args: unknown[]): void; + + /** + * Passes in the system/subsystem filters from the global environment variables. + * This lets the backend merge with any native ones. + * + * @param filters A list of wildcards matching systems or system:subsystem pairs. + */ + setFilters(filters: string[]): void; +} + +/** + * The base class for debug logging backends. It's possible to use this, but the + * same non-guarantees above still apply (unstable interface, etc). + * + * @private + * @internal + */ +export abstract class DebugLogBackendBase implements DebugLogBackend { + cached = new Map(); + filters: string[] = []; + filtersSet = false; + + constructor() { + // Look for the Node config variable for what systems to enable. We'll store + // these for the log method below, which will call setFilters() once. + let nodeFlag = process.env[env.nodeEnables] ?? '*'; + if (nodeFlag === 'all') { + nodeFlag = '*'; + } + this.filters = nodeFlag.split(','); + } + + /** + * Creates a callback function that we can call to send log lines out. + * + * @param namespace The system/subsystem namespace. + */ + abstract makeLogger(namespace: string): AdhocDebugLogCallable; + + /** + * Provides a callback for the subclass to hook if it needs to do something + * specific with `this.filters`. + */ + abstract setFilters(): void; + + log(namespace: string, fields: LogFields, ...args: unknown[]): void { + try { + if (!this.filtersSet) { + this.setFilters(); + this.filtersSet = true; + } + + let logger = this.cached.get(namespace); + if (!logger) { + logger = this.makeLogger(namespace); + this.cached.set(namespace, logger); + } + logger(fields, ...args); + } catch (e) { + // Silently ignore all errors; we don't want them to interfere with + // the user's running app. + // e; + console.error(e); + } + } +} + +// The basic backend. This one definitely works, but it's less feature-filled. +// +// Rather than using util.debuglog, this implements the same basic logic directly. +// The reason for this decision is that debuglog checks the value of the +// NODE_DEBUG environment variable before any user code runs; we therefore +// can't pipe our own enables into it (and util.debuglog will never print unless +// the user duplicates it into NODE_DEBUG, which isn't reasonable). +// +class NodeBackend extends DebugLogBackendBase { + // Default to allowing all systems, since we gate earlier based on whether the + // variable is empty. + enabledRegexp = /.*/g; + + isEnabled(namespace: string): boolean { + return this.enabledRegexp.test(namespace); + } + + makeLogger(namespace: string): AdhocDebugLogCallable { + if (!this.enabledRegexp.test(namespace)) { + return () => {}; + } + + return (fields: LogFields, ...args: unknown[]) => { + // TODO: `fields` needs to be turned into a string here, one way or another. + const nscolour = `${Colours.green}${namespace}${Colours.reset}`; + const pid = `${Colours.yellow}${process.pid}${Colours.reset}`; + let level: string; + switch (fields.severity) { + case LogSeverity.ERROR: + level = `${Colours.red}${fields.severity}${Colours.reset}`; + break; + case LogSeverity.INFO: + level = `${Colours.magenta}${fields.severity}${Colours.reset}`; + break; + case LogSeverity.WARNING: + level = `${Colours.yellow}${fields.severity}${Colours.reset}`; + break; + default: + level = fields.severity ?? LogSeverity.DEFAULT; + break; + } + const msg = util.formatWithOptions({colors: Colours.enabled}, ...args); + + const filteredFields: LogFields = Object.assign({}, fields); + delete filteredFields.severity; + const fieldsJson = Object.getOwnPropertyNames(filteredFields).length + ? JSON.stringify(filteredFields) + : ''; + const fieldsColour = fieldsJson + ? `${Colours.grey}${fieldsJson}${Colours.reset}` + : ''; + + console.error( + '%s [%s|%s] %s%s', + pid, + nscolour, + level, + msg, + fieldsJson ? ` ${fieldsColour}` : '' + ); + }; + } + + // Regexp patterns below are from here: + // https://github.com/nodejs/node/blob/c0aebed4b3395bd65d54b18d1fd00f071002ac20/lib/internal/util/debuglog.js#L36 + setFilters(): void { + const totalFilters = this.filters.join(','); + const regexp = totalFilters + .replace(/[|\\{}()[\]^$+?.]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/,/g, '$|^'); + this.enabledRegexp = new RegExp(`^${regexp}$`, 'i'); + } +} + +/** + * @returns A backend based on Node util.debuglog; this is the default. + */ +export function getNodeBackend(): DebugLogBackend { + return new NodeBackend(); +} + +// Based on the npm "debug" package. Adds colour, time offsets, and other +// useful things, but requires the user to import another package. +// +// Note: using the proper types here introduces an extra dependency +// we don't want, so for the moment, they'll be notational only. +type DebugPackage = any; // debug.Debug +class DebugBackend extends DebugLogBackendBase { + debugPkg: DebugPackage; + + constructor(pkg: DebugPackage) { + super(); + this.debugPkg = pkg; + } + + makeLogger(namespace: string): AdhocDebugLogCallable { + const debugLogger = this.debugPkg(namespace); + return (fields: LogFields, ...args: unknown[]) => { + // TODO: `fields` needs to be turned into a string here. + debugLogger(args[0] as string, ...args.slice(1)); + }; + } + + setFilters(): void { + const existingFilters = process.env['NODE_DEBUG'] ?? ''; + process.env['NODE_DEBUG'] = `${existingFilters}${ + existingFilters ? ',' : '' + }${this.filters.join(',')}`; + } +} + +/** + * Creates a "debug" package backend. The user must call require('debug') and pass + * the resulting object to this function. + * + * ``` + * setBackend(getDebugBackend(require('debug'))) + * ``` + * + * https://www.npmjs.com/package/debug + * + * Note: Google does not explicitly endorse or recommend this package; it's just + * being provided as an option. + * + * @returns A backend based on the npm "debug" package. + */ +export function getDebugBackend(debugPkg: DebugPackage): DebugLogBackend { + return new DebugBackend(debugPkg); +} + +/** + * This pretty much works like the Node logger, but it outputs structured + * logging JSON matching Google Cloud's ingestion specs. Rather than handling + * its own output, it wraps another backend. The passed backend must be a subclass + * of `DebugLogBackendBase` (any of the backends exposed by this package will work). + */ +class StructuredBackend extends DebugLogBackendBase { + upstream: DebugLogBackendBase; + + constructor(upstream?: DebugLogBackend) { + super(); + this.upstream = (upstream as DebugLogBackendBase) ?? new NodeBackend(); + } + + makeLogger(namespace: string): AdhocDebugLogCallable { + const debugLogger = this.upstream.makeLogger(namespace); + return (fields: LogFields, ...args: unknown[]) => { + const severity = fields.severity ?? LogSeverity.INFO; + const json = Object.assign( + { + severity, + message: util.format(...args), + }, + fields + ); + const jsonString = JSON.stringify(json); + + debugLogger(fields, jsonString); + }; + } + + setFilters(): void { + this.upstream.setFilters(); + } +} + +/** + * Creates a "structured logging" backend. This pretty much works like the + * Node logger, but it outputs structured logging JSON matching Google + * Cloud's ingestion specs instead of plain text. + * + * ``` + * setBackend(getStructuredBackend()) + * ``` + * + * @param upstream If you want to use something besides the Node backend to + * write the actual log lines into, pass that here. + * @returns A backend based on Google Cloud structured logging. + */ +export function getStructuredBackend( + upstream?: DebugLogBackend +): DebugLogBackend { + return new StructuredBackend(upstream); +} + +/** + * The environment variables that we standardized on, for all ad-hoc logging. + */ +export const env = { + /** + * Filter wildcards specific to the Node syntax, and similar to the built-in + * utils.debuglog() environment variable. If missing, disables logging. + */ + nodeEnables: 'GOOGLE_SDK_NODE_LOGGING', +}; + +// Keep a copy of all namespaced loggers so users can reliably .on() them. +// Note that these cached functions will need to deal with changes in the backend. +const loggerCache = new Map(); + +// Our current global backend. This might be: +let cachedBackend: DebugLogBackend | null | undefined = undefined; + +/** + * Set the backend to use for our log output. + * - A backend object + * - null to disable logging + * - undefined for "nothing yet", defaults to the Node backend + * + * @param backend Results from one of the get*Backend() functions. + */ +export function setBackend(backend: DebugLogBackend | null) { + cachedBackend = backend; + loggerCache.clear(); +} + +/** + * Creates a logging function. Multiple calls to this with the same namespace + * will produce the same logger, with the same event emitter hooks. + * + * Namespaces can be a simple string ("system" name), or a qualified string + * (system:subsystem), which can be used for filtering, or for "system:*". + * + * @param namespace The namespace, a descriptive text string. + * @returns A function you can call that works similar to console.log(). + */ +export function log( + namespace: string, + parent?: AdhocDebugLogFunction +): AdhocDebugLogFunction { + // If the enable flag isn't set, do nothing. + const enablesFlag = process.env[env.nodeEnables]; + if (!enablesFlag) { + return placeholder; + } + + // This might happen mostly if the typings are dropped in a user's code, + // or if they're calling from JavaScript. + if (!namespace) { + return placeholder; + } + + // Handle sub-loggers. + if (parent) { + namespace = `${parent.instance.namespace}:${namespace}`; + } + + // Reuse loggers so things like event sinks are persistent. + const existing = loggerCache.get(namespace); + if (existing) { + return existing.func; + } + + // Do we have a backend yet? + if (cachedBackend === null) { + // Explicitly disabled. + return placeholder; + } else if (cachedBackend === undefined) { + // One hasn't been made yet, so default to Node. + cachedBackend = getNodeBackend(); + } + + // The logger is further wrapped so we can handle the backend changing out. + const logger: AdhocDebugLogger = (() => { + let previousBackend: DebugLogBackend | undefined = undefined; + const newLogger = new AdhocDebugLogger( + namespace, + (fields: LogFields, ...args: unknown[]) => { + if (previousBackend !== cachedBackend) { + // Did the user pass a custom backend? + if (cachedBackend === null) { + // Explicitly disabled. + return; + } else if (cachedBackend === undefined) { + // One hasn't been made yet, so default to Node. + cachedBackend = getNodeBackend(); + } + + previousBackend = cachedBackend; + } + + cachedBackend?.log(namespace, fields, ...args); + } + ); + return newLogger; + })(); + + loggerCache.set(namespace, logger); + return logger.func; +} diff --git a/logging-utils/src/temporal.ts b/logging-utils/src/temporal.ts new file mode 100644 index 000000000..9cbc06aa5 --- /dev/null +++ b/logging-utils/src/temporal.ts @@ -0,0 +1,96 @@ +// Copyright 2022-2024 Google LLC +// +// 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. + +/* + * We'd like to use the tc39 Temporal standard, but it isn't out of + * proposal, still, and won't be present in all versions of Node until + * much later. Since even some of the polyfills aren't compatible with + * all of our supported versions of Node, this is a very simplified + * version of the pieces we need. When we're ready to turn on the tap + * for the built-in, we'll just export that here instead of this. + */ + +/** + * Simplified interface analogous to the tc39 Temporal.Duration + * parameter to from(). This doesn't support the full gamut (years, days). + */ +export interface DurationLike { + hours?: number; + minutes?: number; + seconds?: number; + millis?: number; +} + +/** + * Simplified list of values to pass to Duration.totalOf(). This + * list is taken from the tc39 Temporal.Duration proposal, but + * larger and smaller units have been left off. + */ +export type TotalOfUnit = 'hour' | 'minute' | 'second' | 'millisecond'; + +/** + * Duration class with an interface similar to the tc39 Temporal + * proposal. Since it's not fully finalized, and polyfills have + * inconsistent compatibility, for now this shim class will be + * used to set durations in Pub/Sub. + * + * This class will remain here for at least the next major version, + * eventually to be replaced by the tc39 Temporal built-in. + * + * https://tc39.es/proposal-temporal/docs/duration.html + */ +export class Duration { + private millis: number; + + private static secondInMillis = 1000; + private static minuteInMillis = Duration.secondInMillis * 60; + private static hourInMillis = Duration.minuteInMillis * 60; + + private constructor(millis: number) { + this.millis = millis; + } + + /** + * Calculates the total number of units of type 'totalOf' that would + * fit inside this duration. + */ + totalOf(totalOf: TotalOfUnit): number { + switch (totalOf) { + case 'hour': + return this.millis / Duration.hourInMillis; + case 'minute': + return this.millis / Duration.minuteInMillis; + case 'second': + return this.millis / Duration.secondInMillis; + case 'millisecond': + return this.millis; + default: + throw new Error(`Invalid unit in call to totalOf(): ${totalOf}`); + } + } + + /** + * Creates a Duration from a DurationLike, which is an object + * containing zero or more of the following: hours, seconds, + * minutes, millis. + */ + static from(durationLike: DurationLike): Duration { + let millis = durationLike.millis ?? 0; + millis += (durationLike.seconds ?? 0) * Duration.secondInMillis; + millis += (durationLike.minutes ?? 0) * Duration.minuteInMillis; + millis += (durationLike.hours ?? 0) * Duration.hourInMillis; + + return new Duration(millis); + } +} diff --git a/logging-utils/test/logging-utils.ts b/logging-utils/test/logging-utils.ts new file mode 100644 index 000000000..0ebec137c --- /dev/null +++ b/logging-utils/test/logging-utils.ts @@ -0,0 +1,257 @@ +// Copyright 2024 Google LLC +// +// 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. + +import {describe, it} from 'mocha'; +import * as assert from 'assert'; + +import * as al from '../src/logging-utils'; + +interface TestLog { + namespace: string; + fields: al.LogFields; + args: unknown[]; +} + +class TestSink extends al.DebugLogBackendBase { + logs: TestLog[] = []; + + makeLogger(namespace: string): al.AdhocDebugLogCallable { + return (fields: al.LogFields, ...args: unknown[]) => { + this.logs.push({namespace, fields, args}); + }; + } + + setFilters(): void {} + + reset() { + this.filters = []; + this.logs = []; + } +} + +describe('adhoc-logging', () => { + const sink: TestSink = new TestSink(); + + describe('Disabled', () => { + const system = 'disabled'; + + beforeEach(() => { + al.setBackend(sink); + sink.reset(); + }); + + it('obeys a lack of global enable', () => { + delete process.env[al.env.nodeEnables]; + const logger = al.log(system); + logger({}, 'foo'); + assert.deepStrictEqual(sink.logs, []); + }); + + it('obeys "all" as an alias for "*"', () => { + process.env[al.env.nodeEnables] = 'all'; + const logger = al.log(system); + logger({}, 'foo'); + assert.deepStrictEqual(sink.logs, [ + { + namespace: system, + fields: {}, + args: ['foo'], + }, + ]); + }); + }); + + describe('Basic enabled', () => { + let logger: al.AdhocDebugLogFunction; + const system = 'basic'; + + beforeEach(() => { + al.setBackend(sink); + sink.reset(); + + process.env[al.env.nodeEnables] = '*'; + logger = al.log(system); + }); + + it('logs with empty fields', () => { + logger({}, 'test log', 5, {other: 'foo'}); + assert.deepStrictEqual(sink.logs, [ + { + namespace: system, + fields: {}, + args: ['test log', 5, {other: 'foo'}], + }, + ]); + }); + + it('logs with fields', () => { + logger({severity: al.LogSeverity.INFO}, 'test log', 5, {other: 'foo'}); + assert.deepStrictEqual(sink.logs, [ + { + namespace: system, + fields: {severity: al.LogSeverity.INFO}, + args: ['test log', 5, {other: 'foo'}], + }, + ]); + }); + + it('logs with severity', () => { + logger.info('test info'); + logger.warn('test warn'); + logger.debug('test debug'); + logger.error('test error'); + assert.deepStrictEqual(sink.logs, [ + { + namespace: system, + fields: {severity: al.LogSeverity.INFO}, + args: ['test info'], + }, + { + namespace: system, + fields: {severity: al.LogSeverity.WARNING}, + args: ['test warn'], + }, + { + namespace: system, + fields: {severity: al.LogSeverity.DEBUG}, + args: ['test debug'], + }, + { + namespace: system, + fields: {severity: al.LogSeverity.ERROR}, + args: ['test error'], + }, + ]); + }); + }); + + describe('Caching', () => { + const cached = 'cached'; + + it('saves logger with the same system/namespace', () => { + const logger = al.log(cached); + const logger2 = al.log(cached); + assert.strictEqual(logger, logger2); + }); + + it('deals with the backend being replaced', () => {}); + }); + + describe('Tap', () => { + let logger: al.AdhocDebugLogFunction; + const system = 'taps'; + + beforeEach(() => { + al.setBackend(sink); + sink.reset(); + + process.env[al.env.nodeEnables] = system; + logger = al.log(system); + }); + + it('allows receiving logs', () => { + const received = [{fields: {}, args: [] as unknown[]}]; + received.pop(); + logger.on('log', (fields: al.LogFields, args: unknown[]) => { + received.push({fields, args}); + }); + logger.info('cool cool'); + assert.deepStrictEqual(received, [ + { + fields: {severity: al.LogSeverity.INFO}, + args: ['cool cool'], + }, + ]); + }); + }); + + describe('Structured log', () => { + const system = 'structured'; + const structured = al.getStructuredBackend(sink); + + let logger: al.AdhocDebugLogFunction; + beforeEach(() => { + al.setBackend(structured); + sink.reset(); + + process.env[al.env.nodeEnables] = system; + + logger = al.log(system); + }); + + it('logs with severity', () => { + logger.info('test info'); + logger.warn('test warn'); + logger.debug('test debug'); + logger.error('test error'); + + assert.deepStrictEqual(sink.logs, [ + { + args: ['{"severity":"INFO","message":"test info"}'], + fields: { + severity: 'INFO', + }, + namespace: 'structured', + }, + { + args: ['{"severity":"WARNING","message":"test warn"}'], + fields: { + severity: 'WARNING', + }, + namespace: 'structured', + }, + { + args: ['{"severity":"DEBUG","message":"test debug"}'], + fields: { + severity: 'DEBUG', + }, + namespace: 'structured', + }, + { + args: ['{"severity":"ERROR","message":"test error"}'], + fields: { + severity: 'ERROR', + }, + namespace: 'structured', + }, + ]); + }); + }); + + describe('sub-logs', () => { + let logger: al.AdhocDebugLogFunction; + const system = 'sublogs'; + const subsystem = 'subsys'; + + beforeEach(() => { + al.setBackend(sink); + sink.reset(); + + process.env[al.env.nodeEnables] = system; + logger = al.log(system); + }); + + it('create with the log sub-function ', () => { + const sublogger = logger.sublog(subsystem); + sublogger({}, 'test log', 5, {other: 'foo'}); + assert.deepStrictEqual(sink.logs, [ + { + namespace: `${system}:${subsystem}`, + fields: {}, + args: ['test log', 5, {other: 'foo'}], + }, + ]); + }); + }); +}); diff --git a/logging-utils/test/temporal.ts b/logging-utils/test/temporal.ts new file mode 100644 index 000000000..a51df456f --- /dev/null +++ b/logging-utils/test/temporal.ts @@ -0,0 +1,38 @@ +// Copyright 2022-2024 Google LLC +// +// 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. + +import {describe, it} from 'mocha'; +import {Duration} from '../src/temporal'; +import * as assert from 'assert'; + +describe('temporal', () => { + describe('Duration', () => { + it('can be created from millis', () => { + const duration = Duration.from({millis: 1234}); + assert.strictEqual(duration.totalOf('second'), 1.234); + }); + it('can be created from seconds', () => { + const duration = Duration.from({seconds: 1.234}); + assert.strictEqual(duration.totalOf('millisecond'), 1234); + }); + it('can be created from minutes', () => { + const duration = Duration.from({minutes: 30}); + assert.strictEqual(duration.totalOf('hour'), 0.5); + }); + it('can be created from hours', () => { + const duration = Duration.from({hours: 1.5}); + assert.strictEqual(duration.totalOf('minute'), 90); + }); + }); +}); diff --git a/logging-utils/tsconfig.json b/logging-utils/tsconfig.json new file mode 100644 index 000000000..78c2cdb3a --- /dev/null +++ b/logging-utils/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "./node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "lib": ["es2018", "dom"], + "rootDir": ".", + "outDir": "build", + "noImplicitAny": true, + "resolveJsonModule": true, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + }, + "include": [ + "src/*.ts", + "test/*.ts" + ] +} diff --git a/release-please-config.json b/release-please-config.json index d1c817a69..ba7ab1d3f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,7 +8,8 @@ "prerelease": false, "packages": { "gax": {}, - "tools": {} + "tools": {}, + "logging-utils": {} }, "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" } \ No newline at end of file