diff --git a/package-lock.json b/package-lock.json index aef68f63..7eed6c0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14204,7 +14204,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.3.0.tgz", "integrity": "sha512-E5303mzwQ+4j/n2J0rDvEPBN7GKjhis10oHiYOgjxsmxYgqG++hz9NyLLOXttzH8as/DyiBHYpUrJTZWYaMo8Q==", - "dev": true, "engines": { "node": ">=10.0.0" } @@ -18673,14 +18672,8 @@ "link": true }, "node_modules/mongodb-log-writer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mongodb-log-writer/-/mongodb-log-writer-1.4.2.tgz", - "integrity": "sha512-ZQYBgW1IZtuRWsv9hF0mjpfDg6usMawkkWKnFb/goXosbW0YyxVA9OHt32TJfppGK/U2p7jQx4V9DxRFsFmmKw==", - "dev": true, - "dependencies": { - "bson": "^4.5.1 || ^5.0.0 || ^6.0.0", - "heap-js": "^2.3.0" - } + "resolved": "packages/mongodb-log-writer", + "link": true }, "node_modules/mongodb-query-parser": { "resolved": "packages/query-parser", @@ -25971,6 +25964,17 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "packages/devtools-connect/node_modules/mongodb-log-writer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mongodb-log-writer/-/mongodb-log-writer-1.4.2.tgz", + "integrity": "sha512-ZQYBgW1IZtuRWsv9hF0mjpfDg6usMawkkWKnFb/goXosbW0YyxVA9OHt32TJfppGK/U2p7jQx4V9DxRFsFmmKw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bson": "^4.5.1 || ^5.0.0 || ^6.0.0", + "heap-js": "^2.3.0" + } + }, "packages/devtools-connect/node_modules/nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", @@ -26560,6 +26564,286 @@ "webidl-conversions": "^3.0.0" } }, + "packages/mongodb-log-writer": { + "version": "2.0.0", + "license": "Apache-2.0", + "dependencies": { + "bson": "^6.8.0", + "heap-js": "^2.3.0" + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.4", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.2", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/semver": "^7.5.0", + "@types/sinon-chai": "^3.2.5", + "acorn": "^8.8.0", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "2.3.2", + "sinon": "^9.2.3", + "typescript": "^5.0.4" + } + }, + "packages/mongodb-log-writer/node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "packages/mongodb-log-writer/node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/mongodb-log-writer/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "packages/mongodb-log-writer/node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "packages/mongodb-log-writer/node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "packages/mongodb-log-writer/node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "packages/mongodb-log-writer/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "packages/mongodb-log-writer/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/mongodb-log-writer/node_modules/mocha": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" + } + }, + "packages/mongodb-log-writer/node_modules/mocha/node_modules/minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": ">=10" + } + }, + "packages/mongodb-log-writer/node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "packages/mongodb-log-writer/node_modules/nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "packages/mongodb-log-writer/node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "packages/mongodb-log-writer/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "packages/mongodb-log-writer/node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "packages/mongodb-log-writer/node_modules/workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true, + "license": "Apache-2.0" + }, "packages/mongodb-redact": { "version": "1.1.5", "license": "Apache-2.0", @@ -32650,6 +32934,16 @@ } } }, + "mongodb-log-writer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/mongodb-log-writer/-/mongodb-log-writer-1.4.2.tgz", + "integrity": "sha512-ZQYBgW1IZtuRWsv9hF0mjpfDg6usMawkkWKnFb/goXosbW0YyxVA9OHt32TJfppGK/U2p7jQx4V9DxRFsFmmKw==", + "dev": true, + "requires": { + "bson": "^4.5.1 || ^5.0.0 || ^6.0.0", + "heap-js": "^2.3.0" + } + }, "nanoid": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", @@ -40011,8 +40305,7 @@ "heap-js": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/heap-js/-/heap-js-2.3.0.tgz", - "integrity": "sha512-E5303mzwQ+4j/n2J0rDvEPBN7GKjhis10oHiYOgjxsmxYgqG++hz9NyLLOXttzH8as/DyiBHYpUrJTZWYaMo8Q==", - "dev": true + "integrity": "sha512-E5303mzwQ+4j/n2J0rDvEPBN7GKjhis10oHiYOgjxsmxYgqG++hz9NyLLOXttzH8as/DyiBHYpUrJTZWYaMo8Q==" }, "highlight.js": { "version": "11.5.1", @@ -43479,13 +43772,190 @@ } }, "mongodb-log-writer": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mongodb-log-writer/-/mongodb-log-writer-1.4.2.tgz", - "integrity": "sha512-ZQYBgW1IZtuRWsv9hF0mjpfDg6usMawkkWKnFb/goXosbW0YyxVA9OHt32TJfppGK/U2p7jQx4V9DxRFsFmmKw==", - "dev": true, + "version": "file:packages/mongodb-log-writer", "requires": { - "bson": "^4.5.1 || ^5.0.0 || ^6.0.0", - "heap-js": "^2.3.0" + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.4", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.2", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/semver": "^7.5.0", + "@types/sinon-chai": "^3.2.5", + "acorn": "^8.8.0", + "bson": "^6.8.0", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "heap-js": "^2.3.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "2.3.2", + "sinon": "^9.2.3", + "typescript": "^5.0.4" + }, + "dependencies": { + "acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "mocha": { + "version": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", + "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.2.0", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "4.2.1", + "ms": "2.1.3", + "nanoid": "3.3.1", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "which": "2.0.2", + "workerpool": "6.2.0", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "minimatch": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", + "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "nanoid": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", + "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "dev": true + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true + }, + "workerpool": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "dev": true + } } }, "mongodb-query-parser": { diff --git a/packages/mongodb-log-writer/.depcheckrc b/packages/mongodb-log-writer/.depcheckrc new file mode 100644 index 00000000..48bf9af6 --- /dev/null +++ b/packages/mongodb-log-writer/.depcheckrc @@ -0,0 +1,8 @@ +ignores: + - '@mongodb-js/prettier-config-devtools' + - '@mongodb-js/tsconfig-devtools' + - '@types/chai' + - '@types/sinon-chai' + - 'sinon' +ignore-patterns: + - 'dist' diff --git a/packages/mongodb-log-writer/.eslintignore b/packages/mongodb-log-writer/.eslintignore new file mode 100644 index 00000000..85a8a75e --- /dev/null +++ b/packages/mongodb-log-writer/.eslintignore @@ -0,0 +1,2 @@ +.nyc-output +dist diff --git a/packages/mongodb-log-writer/.eslintrc.js b/packages/mongodb-log-writer/.eslintrc.js new file mode 100644 index 00000000..83296d73 --- /dev/null +++ b/packages/mongodb-log-writer/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@mongodb-js/eslint-config-devtools'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig-lint.json'], + }, +}; diff --git a/packages/mongodb-log-writer/.gitignore b/packages/mongodb-log-writer/.gitignore new file mode 100644 index 00000000..1d336b61 --- /dev/null +++ b/packages/mongodb-log-writer/.gitignore @@ -0,0 +1,30 @@ +npm-debug.log +build +dist +node_modules +.DS_Store +.idea +*.swp +*.iml +.cache* +report.json +.user-data +.compiled-sources +expansions.yml +.nvmrc +.vscode +!.vscode/extentions.json +.migration-cache +lerna-debug.log +packages/**/*.tgz +packages/**/package-lock.json +scripts/package-lock.json +configs/**/package-lock.json +tmp +storage +.nyc_output +coverage +.ackrc +env-vars.sh +mongocryptd.pid +mongodb-csfle diff --git a/packages/mongodb-log-writer/.mocharc.js b/packages/mongodb-log-writer/.mocharc.js new file mode 100644 index 00000000..64afeb1f --- /dev/null +++ b/packages/mongodb-log-writer/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-devtools'); diff --git a/packages/mongodb-log-writer/.prettierignore b/packages/mongodb-log-writer/.prettierignore new file mode 100644 index 00000000..4d28df66 --- /dev/null +++ b/packages/mongodb-log-writer/.prettierignore @@ -0,0 +1,3 @@ +.nyc_output +dist +coverage diff --git a/packages/mongodb-log-writer/.prettierrc.json b/packages/mongodb-log-writer/.prettierrc.json new file mode 100644 index 00000000..dfae21d0 --- /dev/null +++ b/packages/mongodb-log-writer/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-devtools" diff --git a/packages/mongodb-log-writer/LICENSE b/packages/mongodb-log-writer/LICENSE new file mode 100644 index 00000000..5e0fd33c --- /dev/null +++ b/packages/mongodb-log-writer/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/packages/mongodb-log-writer/README.md b/packages/mongodb-log-writer/README.md new file mode 100644 index 00000000..40f85708 --- /dev/null +++ b/packages/mongodb-log-writer/README.md @@ -0,0 +1,23 @@ +# mongodb-log-writer + +A library for writing MongoDB logv2 messages. + +```js +import { MongoLogManager, mongoLogId } from 'mongodb-log-writer'; + +const manager = new MongoLogManager({ + directory: os.homedir() + '/.app-logs', + retentionDays: 30, + onwarn: console.warn, + onerror: console.error, + gzip: true +}); +await manager.cleanupOldLogfiles(); + +const writer = manager.createLogWriter(); +writer.info('component', mongoLogId(12345), 'context', 'message', { foo: 'bar' }); +``` + +## LICENSE + +Apache-2.0 diff --git a/packages/mongodb-log-writer/package.json b/packages/mongodb-log-writer/package.json new file mode 100644 index 00000000..0da97adb --- /dev/null +++ b/packages/mongodb-log-writer/package.json @@ -0,0 +1,90 @@ +{ + "name": "mongodb-log-writer", + "description": "A library for writing MongoDB logv2 messages", + "version": "2.0.0", + "author": { + "name": "MongoDB Inc", + "email": "compass@mongodb.com" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/COMPASS/issues", + "email": "compass@mongodb.com" + }, + "homepage": "https://github.com/mongodb-js/devtools-shared", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/devtools-shared.git" + }, + "keywords": [ + "mongodb", + "log", + "logv2" + ], + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "require": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "import": { + "default": "./.esm-wrapper.mjs", + "types": "./dist/index.d.ts" + } + }, + "./mongo-log-writer": { + "default": "./dist/mongo-log-writer.js", + "types": "./dist/mongo-log-writer.d.ts" + }, + "./mongo-log-manager": { + "default": "./dist/mongo-log-manager.js", + "types": "./dist/mongo-log-manager.d.ts" + } + }, + "files": [ + "LICENSE", + "dist", + "package.json", + "README.md", + ".esm-wrapper.mjs" + ], + "scripts": { + "lint": "eslint src/**/*.ts", + "test-only": "nyc mocha --colors -r ts-node/register src/**.spec.ts", + "test": "npm run lint && npm run build && npm run test-only", + "build": "npm run compile-ts && gen-esm-wrapper . ./.esm-wrapper.mjs", + "prepack": "npm run build", + "compile-ts": "tsc -p tsconfig.json" + }, + "license": "Apache-2.0", + "dependencies": { + "heap-js": "^2.3.0" + }, + "peerDependencies": { + "bson": "6.x" + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "0.9.10", + "@mongodb-js/mocha-config-devtools": "^1.0.4", + "@mongodb-js/prettier-config-devtools": "^1.0.1", + "@mongodb-js/tsconfig-devtools": "^1.0.2", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.0.0", + "@types/semver": "^7.5.0", + "@types/sinon-chai": "^3.2.5", + "acorn": "^8.8.0", + "chai": "^4.3.6", + "depcheck": "^1.4.1", + "eslint": "^7.25.0", + "gen-esm-wrapper": "^1.1.0", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "2.3.2", + "sinon": "^9.2.3", + "typescript": "^5.0.4" + } +} diff --git a/packages/mongodb-log-writer/src/index.ts b/packages/mongodb-log-writer/src/index.ts new file mode 100644 index 00000000..759b356e --- /dev/null +++ b/packages/mongodb-log-writer/src/index.ts @@ -0,0 +1,2 @@ +export * from './mongo-log-manager'; +export * from './mongo-log-writer'; diff --git a/packages/mongodb-log-writer/src/mongo-log-manager.spec.ts b/packages/mongodb-log-writer/src/mongo-log-manager.spec.ts new file mode 100644 index 00000000..b4652864 --- /dev/null +++ b/packages/mongodb-log-writer/src/mongo-log-manager.spec.ts @@ -0,0 +1,216 @@ +import { MongoLogManager, mongoLogId } from '.'; +import { ObjectId } from 'bson'; +import { once } from 'events'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import { gunzip, constants as zlibConstants } from 'zlib'; +import { promisify } from 'util'; +import sinon from 'ts-sinon'; +import { expect } from 'chai'; + +describe('MongoLogManager', function () { + let directory: string; + let onwarn: any; + let onerror: any; + let retentionDays: number; + + beforeEach(async function () { + retentionDays = 30; + onwarn = sinon.stub(); + onerror = sinon.stub(); + directory = path.join( + os.tmpdir(), + `log-writer-test-${Math.random()}-${Date.now()}` + ); + await fs.mkdir(directory, { recursive: true }); + }); + afterEach(async function () { + await fs.rmdir(directory, { recursive: true }); + }); + + it('allows creating and writing to log files', async function () { + const manager = new MongoLogManager({ + directory, + retentionDays, + onwarn, + onerror, + }); + + const writer = await manager.createLogWriter(); + expect( + path.relative(directory, writer.logFilePath as string)[0] + ).to.not.equal('.'); + expect((writer.logFilePath as string).includes(writer.logId)).to.equal( + true + ); + + writer.info('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.end(); + await once(writer, 'finish'); + + const log = (await fs.readFile(writer.logFilePath as string, 'utf8')) + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)); + expect(log).to.have.lengthOf(1); + expect(log[0].t.$date).to.be.a('string'); + }); + + it('cleans up old log files when requested', async function () { + retentionDays = 0.000001; // 86.4 ms + const manager = new MongoLogManager({ + directory, + retentionDays, + onwarn, + onerror, + }); + + const writer = await manager.createLogWriter(); + writer.info('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.end(); + await once(writer, 'finish'); + + await fs.stat(writer.logFilePath as string); + await new Promise((resolve) => setTimeout(resolve, 100)); + await manager.cleanupOldLogFiles(); + try { + await fs.stat(writer.logFilePath as string); + expect.fail('missed exception'); + } catch (err: any) { + expect(err.code).to.equal('ENOENT'); + } + }); + + it('cleans up least recent log files when requested', async function () { + const manager = new MongoLogManager({ + directory, + retentionDays, + maxLogFileCount: 5, + onwarn, + onerror, + }); + + const paths: string[] = []; + const offset = Math.floor(Date.now() / 1000); + for (let i = 0; i < 10; i++) { + const filename = path.join( + directory, + ObjectId.createFromTime(offset - i).toHexString() + '_log' + ); + await fs.writeFile(filename, ''); + paths.unshift(filename); + } + + const getFiles = async () => { + return ( + await Promise.all( + paths.map((path) => + fs.stat(path).then( + () => 1, + () => 0 + ) + ) + ) + ).join(''); + }; + expect(await getFiles()).to.equal('1111111111'); + await manager.cleanupOldLogFiles(); + expect(await getFiles()).to.equal('0000011111'); + }); + + it('cleaning up old log files is a no-op by default', async function () { + const manager = new MongoLogManager({ + directory: path.join('directory', 'nonexistent'), + retentionDays, + onwarn, + onerror, + }); + + await manager.cleanupOldLogFiles(); + }); + + it('creates no-op write streams as a fallback', async function () { + const manager = new MongoLogManager({ + directory: path.join('directory', 'nonexistent'), + retentionDays, + onwarn, + onerror, + }); + + const writer = await manager.createLogWriter(); + expect(onwarn).to.have.been.calledOnce; // eslint-disable-line + expect(writer.logFilePath).to.equal(null); + + writer.info('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.end(); + await once(writer, 'finish'); + }); + + it('optionally allow gzip’ed log files', async function () { + const manager = new MongoLogManager({ + directory, + retentionDays, + onwarn, + onerror, + gzip: true, + }); + + const writer = await manager.createLogWriter(); + expect(writer.logFilePath as string).to.match(/\.gz$/); + writer.info('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.end(); + await once(writer, 'log-finish'); + + const log = ( + await promisify(gunzip)(await fs.readFile(writer.logFilePath as string)) + ) + .toString() + .split('\n') + .filter(Boolean) + .map((line: string) => JSON.parse(line)); + expect(log).to.have.lengthOf(1); + expect(log[0].t.$date).to.be.a('string'); + }); + + it('optionally can read truncated gzip’ed log files', async function () { + const manager = new MongoLogManager({ + directory, + retentionDays, + onwarn, + onerror, + gzip: true, + }); + + const writer = await manager.createLogWriter(); + expect(writer.logFilePath as string).to.match(/\.gz$/); + writer.info('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + await writer.flush(); + + const log = ( + await promisify(gunzip)(await fs.readFile(writer.logFilePath as string), { + finishFlush: zlibConstants.Z_SYNC_FLUSH, + }) + ) + .toString() + .split('\n') + .filter(Boolean) + .map((line: string) => JSON.parse(line)); + expect(log).to.have.lengthOf(1); + expect(log[0].t.$date).to.be.a('string'); + + // Still clean up here because Windows doesn’t like open files: + writer.end(); + await once(writer, 'finish'); + }); +}); diff --git a/packages/mongodb-log-writer/src/mongo-log-manager.ts b/packages/mongodb-log-writer/src/mongo-log-manager.ts new file mode 100644 index 00000000..487c6a2d --- /dev/null +++ b/packages/mongodb-log-writer/src/mongo-log-manager.ts @@ -0,0 +1,147 @@ +import path from 'path'; +import { ObjectId } from 'bson'; +import { once } from 'events'; +import { createWriteStream, promises as fs } from 'fs'; +import { createGzip, constants as zlibConstants } from 'zlib'; +import { Heap } from 'heap-js'; +import { MongoLogWriter } from './mongo-log-writer'; +import { Writable } from 'stream'; + +/** Options used by MongoLogManager instances. */ +interface MongoLogOptions { + /** A base directory in which log files are stored. */ + directory: string; + /** Whether to write files as .gz files or not. */ + gzip?: boolean; + /** The number of calendar days after which old log files are deleted. */ + retentionDays: number; + /** The maximal number of log files which are kept. */ + maxLogFileCount?: number; + /** A handler for warnings related to a specific filesystem path. */ + onerror: (err: Error, path: string) => unknown | Promise; + /** A handler for errors related to a specific filesystem path. */ + onwarn: (err: Error, path: string) => unknown | Promise; +} + +/** + * A manger for the log files of an application. + * Log files will be stored in a single directory, following the + * naming convention `${logId}_log`. + */ +export class MongoLogManager { + _options: MongoLogOptions; + + constructor(options: MongoLogOptions) { + this._options = options; + } + + /** Clean up log files older than `retentionDays`. */ + async cleanupOldLogFiles(maxDurationMs = 5_000): Promise { + const dir = this._options.directory; + let dirHandle; + try { + dirHandle = await fs.opendir(dir); + } catch { + return; + } + + const deletionStartTimestamp = Date.now(); + // Delete files older than N days + const deletionCutoffTimestamp = + deletionStartTimestamp - this._options.retentionDays * 86400 * 1000; + // Store the known set of least recent files in a heap in order to be able to + // delete all but the most recent N files. + const leastRecentFileHeap = new Heap<{ + fileTimestamp: number; + fullPath: string; + }>((a, b) => a.fileTimestamp - b.fileTimestamp); + + for await (const dirent of dirHandle) { + // Cap the overall time spent inside this function. Consider situations like + // a large number of machines using a shared network-mounted $HOME directory + // where lots and lots of log files end up and filesystem operations happen + // with network latency. + if (Date.now() - deletionStartTimestamp > maxDurationMs) break; + + if (!dirent.isFile()) continue; + const { id } = + /^(?[a-f0-9]{24})_log(\.gz)?$/i.exec(dirent.name)?.groups ?? {}; + if (!id) continue; + const fileTimestamp = +new ObjectId(id).getTimestamp(); + const fullPath = path.join(dir, dirent.name); + let toDelete: string | undefined; + + // If the file is older than expected, delete it. If the file is recent, + // add it to the list of seen files, and if that list is too large, remove + // the least recent file we've seen so far. + if (fileTimestamp < deletionCutoffTimestamp) { + toDelete = fullPath; + } else if (this._options.maxLogFileCount) { + leastRecentFileHeap.push({ fullPath, fileTimestamp }); + if (leastRecentFileHeap.size() > this._options.maxLogFileCount) { + toDelete = leastRecentFileHeap.pop()?.fullPath; + } + } + + if (!toDelete) continue; + try { + await fs.unlink(toDelete); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + if (err?.code !== 'ENOENT') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this._options.onerror(err, fullPath); + } + } + } + } + + /** Create a MongoLogWriter stream for a new log file. */ + async createLogWriter(): Promise { + const logId = new ObjectId().toString(); + const doGzip = !!this._options.gzip; + const logFilePath = path.join( + this._options.directory, + `${logId}_log${doGzip ? '.gz' : ''}` + ); + + let originalTarget: Writable; + let stream: Writable; + let logWriter: MongoLogWriter | undefined; + try { + stream = createWriteStream(logFilePath, { mode: 0o600 }); + originalTarget = stream; + await once(stream, 'ready'); + if (doGzip) { + stream = createGzip({ + flush: zlibConstants.Z_SYNC_FLUSH, + level: zlibConstants.Z_MAX_LEVEL, + }); + stream.pipe(originalTarget); + } else { + stream.on('finish', () => stream.emit('log-finish')); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (err: any) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this._options.onwarn(err, logFilePath); + stream = new Writable({ + write(chunk, enc, cb) { + // Just ignore log data if there was an error. + cb(); + }, + }); + originalTarget = stream; + logWriter = new MongoLogWriter(logId, null, stream); + } + if (!logWriter) { + logWriter = new MongoLogWriter(logId, logFilePath, stream); + } + + // We use 'log-finish' to give consumers an event that they can + // listen on which is always only emitted once data has actually + // been written to disk. + originalTarget.on('finish', () => logWriter?.emit('log-finish')); + return logWriter; + } +} diff --git a/packages/mongodb-log-writer/src/mongo-log-writer.spec.ts b/packages/mongodb-log-writer/src/mongo-log-writer.spec.ts new file mode 100644 index 00000000..7f03f654 --- /dev/null +++ b/packages/mongodb-log-writer/src/mongo-log-writer.spec.ts @@ -0,0 +1,180 @@ +import type { MongoLogEntry } from '.'; +import { MongoLogWriter, mongoLogId } from '.'; +import { EJSON } from 'bson'; +import stream from 'stream'; +import { inspect } from 'util'; +import chai, { expect } from 'chai'; +import sinonChai from 'sinon-chai'; +chai.use(sinonChai); + +describe('MongoLogWriter', function () { + it('allows writing log messages to a stream', async function () { + const now = new Date(1628591965386); + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target, () => now); + const logEvents: MongoLogEntry[] = []; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + writer.on('log', (entry) => logEvents.push(entry)); + expect(writer.target).to.equal(target); + writer.info('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.warn('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.error('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.fatal('component', mongoLogId(12345), 'context', 'message', { + foo: 'bar', + }); + writer.debug( + 'component', + mongoLogId(12345), + 'context', + 'message', + { foo: 'bar' }, + 2 + ); + writer.write({ + t: now, + s: 'E', + c: 'x', + id: mongoLogId(0), + ctx: 'y', + msg: 'z', + }); + await writer.flush(); + const log = target + .read() + .split('\n') + .filter(Boolean) + .map((line: string) => JSON.parse(line)); + expect(log).to.deep.equal(EJSON.serialize(logEvents)); + expect(log).to.deep.equal([ + { + t: { $date: '2021-08-10T10:39:25.386Z' }, + s: 'I', + c: 'component', + id: 12345, + ctx: 'context', + msg: 'message', + attr: { foo: 'bar' }, + }, + { + t: { $date: '2021-08-10T10:39:25.386Z' }, + s: 'W', + c: 'component', + id: 12345, + ctx: 'context', + msg: 'message', + attr: { foo: 'bar' }, + }, + { + t: { $date: '2021-08-10T10:39:25.386Z' }, + s: 'E', + c: 'component', + id: 12345, + ctx: 'context', + msg: 'message', + attr: { foo: 'bar' }, + }, + { + t: { $date: '2021-08-10T10:39:25.386Z' }, + s: 'F', + c: 'component', + id: 12345, + ctx: 'context', + msg: 'message', + attr: { foo: 'bar' }, + }, + { + t: { $date: '2021-08-10T10:39:25.386Z' }, + s: 'D2', + c: 'component', + id: 12345, + ctx: 'context', + msg: 'message', + attr: { foo: 'bar' }, + }, + { + t: { $date: '2021-08-10T10:39:25.386Z' }, + s: 'E', + c: 'x', + id: 0, + ctx: 'y', + msg: 'z', + }, + ]); + }); + + it('can log error object as data as-is', async function () { + const now = new Date(1628591965386); + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target, () => now); + writer.error( + 'component', + mongoLogId(12345), + 'context', + 'message', + new Error('foo') + ); + await writer.flush(); + const log = target + .read() + .split('\n') + .filter(Boolean) + .map((line: string) => JSON.parse(line)); + log[0].attr.stack = ''; + expect(log[0].attr).to.deep.equal({ + code: null, + message: 'foo', + name: 'Error', + stack: '', + }); + }); + + it('can log non-trivial data', async function () { + const now = new Date(1628591965386); + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target, () => now); + + const cyclic: any = {}; + cyclic.cyclic = cyclic; + writer.error('component', mongoLogId(12345), 'context', 'message', cyclic); + + await writer.flush(); + const log = target + .read() + .split('\n') + .filter(Boolean) + .map((line: string) => JSON.parse(line).attr); + expect(log).to.deep.equal([ + { + _inspected: inspect(cyclic), + }, + ]); + }); + + it('rejects invalid messages', async function () { + const errors: Error[] = []; + function tryWrite(input: any) { + const target = new stream.PassThrough().setEncoding('utf8'); + const writer = new MongoLogWriter('logid', null, target); + writer.on('error', (err) => errors.push(err)); + writer.write(input); + } + tryWrite({}); + tryWrite({ s: 'E' }); + tryWrite({ s: 'E', c: '' }); + tryWrite({ s: 'E', c: '', id: mongoLogId(0) }); + tryWrite({ s: 'E', c: '', id: mongoLogId(0), ctx: '' }); + tryWrite({ s: 'E', c: '', id: mongoLogId(0), ctx: '', msg: '' }); + + await new Promise(setImmediate); + expect(errors).to.have.lengthOf(5); + expect(new Set([...errors.map((err) => err.name)])).to.deep.equal( + new Set(['TypeError']) + ); + }); +}); diff --git a/packages/mongodb-log-writer/src/mongo-log-writer.ts b/packages/mongodb-log-writer/src/mongo-log-writer.ts new file mode 100644 index 00000000..befabacf --- /dev/null +++ b/packages/mongodb-log-writer/src/mongo-log-writer.ts @@ -0,0 +1,350 @@ +import { EJSON } from 'bson'; +import { Writable } from 'stream'; +import { inspect } from 'util'; + +type PlainWritable = Pick; + +/** + * A unique correlation ID for log lines. Always create these + * using {@link mongoLogId()}, never directly. + */ +export interface MongoLogId { + /** @internal */ + __value: number; +} + +/** Create an ID for a given log line. */ +export function mongoLogId(id: number): MongoLogId { + return { __value: id }; +} + +/** + * An unformatted MongoDB log entry. + * @see {@link https://www.mongodb.com/docs/manual/reference/log-messages/#structured-logging} + */ +export interface MongoLogEntry { + /** Timestamp at which the log event occurred */ + t?: Date; + /** Severity field */ + s: 'F' | 'E' | 'W' | 'I' | 'D1' | 'D2' | 'D3' | 'D4' | 'D5'; + /** Component field */ + c: string; + /** The message id field */ + id: MongoLogId; + /** The context field */ + ctx: string; + /** The message string field */ + msg: string; + /** Additional information about the event in question */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + attr?: any; +} + +/** + * Verify that a given {@link MongoLogEntry} contains all necessary fields. + * @returns Either a TypeError if the log entry is invalid, or null. + */ +function validateLogEntry(info: MongoLogEntry): Error | null { + if (typeof info.s !== 'string') { + return new TypeError('Cannot log messages without a severity field'); + } + if (typeof info.c !== 'string') { + return new TypeError('Cannot log messages without a component field'); + } + if (typeof info.id?.__value !== 'number') { + return new TypeError('Cannot log messages without an id field'); + } + if (typeof info.ctx !== 'string') { + return new TypeError('Cannot log messages without a context field'); + } + if (typeof info.msg !== 'string') { + return new TypeError('Cannot log messages without a message field'); + } + return null; +} + +/** + * A helper class for writing formatted log information to an output stream. + * This class itself is an object-mode Writable stream to which + * {@link MongoLogEntry} objects can be written. + * + * This class does not do any I/O of its own, and only delegates that to + * the target stream. + */ +export class MongoLogWriter extends Writable { + _logId: string; + _logFilePath: string | null; + _target: PlainWritable; + _now: () => Date; + + /** + * @param logId A unique identifier for this log file. This is not used outside the `logId` getter. + * @param logFilePath The target path for this log file, if any. This is not used outside the `logFilePath` getter. + * @param target The Writable stream to write data to. + * @param now An optional function that overrides computation of the current time. This is used for testing. + */ + constructor( + logId: string, + logFilePath: string | null, + target: PlainWritable, + now?: () => Date + ) { + super({ objectMode: true }); + this._logId = logId; + this._logFilePath = logFilePath; + this._target = target; + this._now = now ?? (() => new Date()); + } + + /** Return the logId passed to the constructor. */ + get logId(): string { + return this._logId; + } + + /** Return the logFilePath passed to the constructor. */ + get logFilePath(): string | null { + return this._logFilePath; + } + + /** Return the target stream that was used to create this MongoLogWriter instance. */ + get target(): PlainWritable { + return this._target; + } + + _write( + info: MongoLogEntry, + encoding: unknown, + callback: (err?: Error | null | undefined) => void + ): void { + const validationError = validateLogEntry(info); + if (validationError) { + callback(validationError); + return; + } + + // Copy the object to ensure the order of properties. + const fullInfo: Omit & { id: number } = { + t: info.t ?? this._now(), + s: info.s, + c: info.c, + id: info.id.__value, + ctx: info.ctx, + msg: info.msg, + }; + + if (info.attr) { + if (Object.prototype.toString.call(info.attr) === '[object Error]') { + fullInfo.attr = { + stack: info.attr.stack, + name: info.attr.name, + message: info.attr.message, + code: info.attr.code, + ...info.attr, + }; + } else { + fullInfo.attr = info.attr; + } + } + + this.emit('log', fullInfo); + + // The attr field may contain arbitrary data. If we cannot serialize it, + // we fall back to increasingly less faithful representations of it. + try { + EJSON.stringify(fullInfo.attr); + } catch { + try { + // This package may be running in a web environment + // where v8 is not available. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const v8 = require('v8'); + const cloned = v8.deserialize(v8.serialize(fullInfo.attr)); + EJSON.stringify(cloned); + fullInfo.attr = cloned; + } catch { + try { + const cloned = JSON.parse(JSON.stringify(fullInfo.attr)); + EJSON.stringify(cloned); + fullInfo.attr = cloned; + } catch { + fullInfo.attr = { _inspected: inspect(fullInfo.attr) }; + } + } + } + this._target.write( + EJSON.stringify(fullInfo, { relaxed: true }) + '\n', + callback + ); + } + + _final(callback: (err?: Error | null | undefined) => void): void { + this._target.end(callback); + } + + /** Wait until all pending data has been written to the underlying stream. */ + async flush(): Promise { + await new Promise((resolve) => this._target.write('', resolve)); + } + + /** + * Write a log entry with severity 'I'. + */ + info( + component: string, + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void { + const logEntry: MongoLogEntry = { + s: 'I', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr, + }; + this.write(logEntry); + } + + /** + * Write a log entry with severity 'W'. + */ + warn( + component: string, + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void { + const logEntry: MongoLogEntry = { + s: 'W', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr, + }; + this.write(logEntry); + } + + /** + * Write a log entry with severity 'E'. + */ + error( + component: string, + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void { + const logEntry: MongoLogEntry = { + s: 'E', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr, + }; + this.write(logEntry); + } + + /** + * Write a log entry with severity 'F'. + */ + fatal( + component: string, + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void { + const logEntry: MongoLogEntry = { + s: 'F', + c: component, + id: id, + ctx: context, + msg: message, + attr: attr, + }; + this.write(logEntry); + } + + /** + * Write a log entry with severity 'D'. + */ + debug( + component: string, + id: MongoLogId, + context: string, + message: string, + attr?: unknown, + level: 1 | 2 | 3 | 4 | 5 = 1 + ): void { + const logEntry: MongoLogEntry = { + s: `D${level}`, + c: component, + id: id, + ctx: context, + msg: message, + attr: attr, + }; + this.write(logEntry); + } + + /** + * Create a MongoLogWriter-like object with a bound 'component' value + */ + bindComponent(component: string): { + unbound: MongoLogWriter; + component: string; + write( + entry: Omit, + cb?: (error?: Error | null) => void + ): boolean; + info( + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void; + warn( + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void; + error( + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void; + fatal( + id: MongoLogId, + context: string, + message: string, + attr?: unknown + ): void; + debug( + id: MongoLogId, + context: string, + message: string, + attr?: unknown, + level?: 1 | 2 | 3 | 4 | 5 + ): void; + } { + return { + unbound: this, + component: component, + write: (entry, cb) => this.write({ c: component, ...entry }, cb), + info: this.info.bind(this, component), + warn: this.warn.bind(this, component), + error: this.error.bind(this, component), + fatal: this.fatal.bind(this, component), + debug: this.debug.bind(this, component), + }; + } + + mongoLogId = mongoLogId; +} diff --git a/packages/mongodb-log-writer/tsconfig-lint.json b/packages/mongodb-log-writer/tsconfig-lint.json new file mode 100644 index 00000000..6bdef84f --- /dev/null +++ b/packages/mongodb-log-writer/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mongodb-log-writer/tsconfig.json b/packages/mongodb-log-writer/tsconfig.json new file mode 100644 index 00000000..836d247a --- /dev/null +++ b/packages/mongodb-log-writer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@mongodb-js/tsconfig-devtools/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["./src/**/*.spec.*"] +}