diff --git a/package-lock.json b/package-lock.json index d186dea6..0eaff0bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7026,6 +7026,10 @@ "resolved": "packages/mongodb-downloader", "link": true }, + "node_modules/@mongodb-js/mongodb-server-log-checker": { + "resolved": "packages/mongodb-server-log-checker", + "link": true + }, "node_modules/@mongodb-js/mongodb-ts-autocomplete": { "resolved": "packages/mongodb-ts-autocomplete", "link": true @@ -30568,6 +30572,30 @@ "node": ">=12" } }, + "packages/mongodb-server-log-checker": { + "name": "@mongodb-js/mongodb-server-log-checker", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "^0.11.3", + "@mongodb-js/mocha-config-devtools": "^1.1.0", + "@mongodb-js/prettier-config-devtools": "^1.0.2", + "@mongodb-js/tsconfig-devtools": "^1.1.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "@types/node": "^22.15.30", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.5.0", + "depcheck": "^1.4.7", + "eslint": "^7.25.0 || ^8.0.0", + "gen-esm-wrapper": "^1.1.3", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^3.5.3", + "sinon": "^9.2.3", + "typescript": "^5.9.3" + } + }, "packages/mongodb-ts-autocomplete": { "name": "@mongodb-js/mongodb-ts-autocomplete", "version": "0.6.5", @@ -37553,6 +37581,28 @@ } } }, + "@mongodb-js/mongodb-server-log-checker": { + "version": "file:packages/mongodb-server-log-checker", + "requires": { + "@mongodb-js/eslint-config-devtools": "^0.11.3", + "@mongodb-js/mocha-config-devtools": "^1.1.0", + "@mongodb-js/prettier-config-devtools": "^1.0.2", + "@mongodb-js/tsconfig-devtools": "^1.1.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "@types/node": "^22.15.30", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.5.0", + "depcheck": "^1.4.7", + "eslint": "^7.25.0 || ^8.0.0", + "gen-esm-wrapper": "^1.1.3", + "mocha": "^8.4.0", + "nyc": "^15.1.0", + "prettier": "^3.5.3", + "sinon": "^9.2.3", + "typescript": "^5.9.3" + } + }, "@mongodb-js/mongodb-ts-autocomplete": { "version": "file:packages/mongodb-ts-autocomplete", "requires": { diff --git a/packages/mongodb-server-log-checker/.depcheckrc b/packages/mongodb-server-log-checker/.depcheckrc new file mode 100644 index 00000000..48bf9af6 --- /dev/null +++ b/packages/mongodb-server-log-checker/.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-server-log-checker/.eslintignore b/packages/mongodb-server-log-checker/.eslintignore new file mode 100644 index 00000000..85a8a75e --- /dev/null +++ b/packages/mongodb-server-log-checker/.eslintignore @@ -0,0 +1,2 @@ +.nyc-output +dist diff --git a/packages/mongodb-server-log-checker/.eslintrc.js b/packages/mongodb-server-log-checker/.eslintrc.js new file mode 100644 index 00000000..83296d73 --- /dev/null +++ b/packages/mongodb-server-log-checker/.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-server-log-checker/.mocharc.js b/packages/mongodb-server-log-checker/.mocharc.js new file mode 100644 index 00000000..64afeb1f --- /dev/null +++ b/packages/mongodb-server-log-checker/.mocharc.js @@ -0,0 +1 @@ +module.exports = require('@mongodb-js/mocha-config-devtools'); diff --git a/packages/mongodb-server-log-checker/.prettierignore b/packages/mongodb-server-log-checker/.prettierignore new file mode 100644 index 00000000..4d28df66 --- /dev/null +++ b/packages/mongodb-server-log-checker/.prettierignore @@ -0,0 +1,3 @@ +.nyc_output +dist +coverage diff --git a/packages/mongodb-server-log-checker/.prettierrc.json b/packages/mongodb-server-log-checker/.prettierrc.json new file mode 100644 index 00000000..dfae21d0 --- /dev/null +++ b/packages/mongodb-server-log-checker/.prettierrc.json @@ -0,0 +1 @@ +"@mongodb-js/prettier-config-devtools" diff --git a/packages/mongodb-server-log-checker/LICENSE b/packages/mongodb-server-log-checker/LICENSE new file mode 100644 index 00000000..5e0fd33c --- /dev/null +++ b/packages/mongodb-server-log-checker/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-server-log-checker/package.json b/packages/mongodb-server-log-checker/package.json new file mode 100644 index 00000000..8c827956 --- /dev/null +++ b/packages/mongodb-server-log-checker/package.json @@ -0,0 +1,74 @@ +{ + "name": "@mongodb-js/mongodb-server-log-checker", + "description": "Utilities for comparing a MongoDB server log against expected warnings", + "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", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "https://github.com/mongodb-js/devtools-shared.git" + }, + "files": [ + "dist" + ], + "license": "Apache-2.0", + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "require": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "import": { + "default": "./dist/.esm-wrapper.mjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "bootstrap": "npm run compile", + "prepublishOnly": "npm run compile", + "compile": "tsc -p tsconfig.json && gen-esm-wrapper . ./dist/.esm-wrapper.mjs", + "typecheck": "tsc --noEmit", + "eslint": "eslint", + "prettier": "prettier", + "lint": "npm run eslint . && npm run prettier -- --check .", + "depcheck": "depcheck", + "check": "npm run typecheck && npm run lint && npm run depcheck", + "check-ci": "npm run check", + "test": "mocha", + "test-cov": "nyc -x \"**/*.spec.*\" --reporter=lcov --reporter=text --reporter=html npm run test", + "test-watch": "npm run test -- --watch", + "test-ci": "npm run test-cov", + "reformat": "npm run prettier -- --write ." + }, + "devDependencies": { + "@mongodb-js/eslint-config-devtools": "^0.11.3", + "@mongodb-js/mocha-config-devtools": "^1.1.0", + "@mongodb-js/prettier-config-devtools": "^1.0.2", + "@mongodb-js/tsconfig-devtools": "^1.1.1", + "@types/chai": "^4.2.21", + "@types/mocha": "^9.1.1", + "@types/node": "^22.15.30", + "@types/sinon-chai": "^3.2.5", + "chai": "^4.5.0", + "depcheck": "^1.4.7", + "eslint": "^7.25.0 || ^8.0.0", + "gen-esm-wrapper": "^1.1.3", + "mocha": "^8.4.0", + "mongodb-runner": "^6.7.1", + "nyc": "^15.1.0", + "prettier": "^3.5.3", + "sinon": "^9.2.3", + "typescript": "^5.9.3" + } +} diff --git a/packages/mongodb-server-log-checker/src/index.spec.ts b/packages/mongodb-server-log-checker/src/index.spec.ts new file mode 100644 index 00000000..b12110a3 --- /dev/null +++ b/packages/mongodb-server-log-checker/src/index.spec.ts @@ -0,0 +1,120 @@ +import { expect } from 'chai'; +import type { LogEntry } from './index'; +import { ServerLogsChecker } from './index'; +import { EventEmitter } from 'events'; +import { MongoCluster } from 'mongodb-runner'; +import path from 'path'; +import { tmpdir } from 'os'; +import { setTimeout as delay } from 'timers/promises'; + +function createLogEntry( + severity: 'W' | 'E' | 'F' | 'I', + id: number, + attr?: any, +): LogEntry { + return { + timestamp: new Date().toISOString(), + severity, + id, + attr, + component: 'testComponent', + context: 'testContext', + message: `Test log entry with ID ${id}`, + }; +} + +describe('ServerLogsChecker', function () { + let logEmitter: EventEmitter; + let checker: ServerLogsChecker; + + beforeEach(function () { + logEmitter = new EventEmitter(); + checker = new ServerLogsChecker(logEmitter); + }); + + afterEach(function () { + checker.close(); + }); + + it('collects warnings, errors, and fatal logs', function () { + const warningEntry = createLogEntry('W', 1001); + const errorEntry = createLogEntry('E', 2001); + const fatalEntry = createLogEntry('F', 3001); + const infoEntry = createLogEntry('I', 4001); // Should be ignored + + logEmitter.emit('mongoLog', 'server', warningEntry); + logEmitter.emit('mongoLog', 'server', errorEntry); + logEmitter.emit('mongoLog', 'server', fatalEntry); + logEmitter.emit('mongoLog', 'server', infoEntry); + + expect(checker.warnings).to.deep.equal([ + warningEntry, + errorEntry, + fatalEntry, + ]); + }); + + it('allows specific warnings by log ID', function () { + const warningEntry = createLogEntry('W', 1001); + checker.allowWarning(1001); + + logEmitter.emit('mongoLog', 'server', warningEntry); + + expect(checker.warnings).to.deep.equal([]); + }); + + it('allows specific warnings by predicate function', function () { + const warningEntry = createLogEntry('W', 1001, { message: 'Test warning' }); + checker.allowWarning((entry) => entry.attr.message === 'Test warning'); + + logEmitter.emit('mongoLog', 'server', warningEntry); + + expect(checker.warnings).to.deep.equal([]); + }); + + it('does not allow warnings that do not match filters', function () { + const warningEntry = createLogEntry('W', 1001); + checker.allowWarning(9999); // Allow a different log ID + + logEmitter.emit('mongoLog', 'server', warningEntry); + + expect(checker.warnings).to.deep.equal([warningEntry]); + }); + + context('runner integration', function () { + let cluster: MongoCluster; + + beforeEach(async function () { + this.timeout(180_000); // Extra time for server binary download + cluster = await MongoCluster.start({ + topology: 'replset', + secondaries: 0, + tmpDir: path.join(tmpdir(), 'mongodb-server-log-checker-test'), + args: ['--setParameter', 'enableTestCommands=1'], + }); + checker = new ServerLogsChecker(cluster); + }); + + afterEach(async function () { + checker?.close(); + await cluster?.close(); + }); + + it('collects warnings from the cluster', async function () { + await cluster.withClient(async (client) => { + // Configuring a fail point is a reliable way to generate a warning log entry + await client.db('admin').command({ + configureFailPoint: 'failCommand', + mode: 'alwaysOn', + data: { errorCode: 2, failCommands: ['find'] }, + }); + }); + await delay(1000); + expect(checker.warnings).to.have.lengthOf.greaterThan(0); + expect(checker.warnings.find((w) => w.id === 23829)).to.exist; + expect(() => checker.noServerWarningsCheckpoint()).to.throw( + /Unexpected server warnings detected/, + ); + }); + }); +}); diff --git a/packages/mongodb-server-log-checker/src/index.ts b/packages/mongodb-server-log-checker/src/index.ts new file mode 100644 index 00000000..b15cddf7 --- /dev/null +++ b/packages/mongodb-server-log-checker/src/index.ts @@ -0,0 +1,176 @@ +// Compatible with mongodb-runner's LogEntry type, but we +// want to avoid depending on the entire package just for that. +export type LogEntry = { + timestamp: string; + severity: string; + component: string; + context: string; + message: string; + id: number | undefined; + attr: any; +}; + +export interface MongoLogEventEmitter { + on( + event: 'mongoLog', + listener: (serverUUID: string, entry: LogEntry) => void, + ): void; + off( + event: 'mongoLog', + listener: (serverUUID: string, entry: LogEntry) => void, + ): void; +} + +/** + * Filter for allowing specific server warnings. + * Can be a numeric log ID or a predicate function. + */ +export type WarningFilter = number | ((entry: LogEntry) => boolean); + +/** + * Monitors MongoDB server logs and validates that no unexpected warnings occur. + * Modeled after the mongosh implementation in PR #2574. + */ +export class ServerLogsChecker { + static defaultAllowedWarnings: WarningFilter[] = [ + 4615610, // "Failed to check socket connectivity", generic disconnect error + 7012500, // "Failed to refresh query analysis configurations", normal sharding behavior + 4906901, // "Arbiters are not supported in quarterly binary versions" + 6100702, // "Failed to get last stable recovery timestamp due to lock acquire timeout. Note this is expected if shutdown is in progress." + 20525, // "Failed to gather storage statistics for slow operation" + 22120, // "Access control is not enabled for the database" + 22140, // "This server is bound to localhost" + 22178, // "transparent_hugepage/enabled is 'always'" + 5123300, // "vm.max_map_count is too low" + 551190, // "Server certificate has no compatible Subject Alternative Name", + 20526, // "Failed to gather storage statistics for slow operation" + 22668, // "Unable to ping distributed locks" + 21764, // "Unable to forward progress" REPL + 22225, // "Flow control is engaged and the sustainer point is not moving. Please check the health of all secondaries." + 2658100, // "Hinted index could not provide a bounded scan, reverting to whole index scan" + (l: LogEntry) => { + // "Use of deprecated server parameter name" (FTDC) + return (l.id === 636300 || l.id === 23803) && l.context === 'ftdc'; + }, + (l: LogEntry) => l.component === 'STORAGE', // Outside of mongosh's control + (l: LogEntry) => l.context === 'BackgroundSync', // Outside of mongosh's control + (l: LogEntry) => { + // "Aggregate command executor error", we get this a lot for things like + // $collStats which internally tries to open collections that may or may not exist + return ( + l.id === 23799 && + [ + 'NamespaceNotFound', + 'ShardNotFound', + 'CommandNotSupportedOnView', + ].includes(l.attr?.error?.codeName) + ); + }, + (l: LogEntry) => { + // "getMore command executor error" can happen under normal circumstances + // for client errors + return l.id === 20478 && l.attr?.error?.codeName === 'ClientDisconnect'; + }, + (l: LogEntry) => { + // "$jsonSchema validator does not allow '_id' field" warning + // incorrectly issued for the implicit schema of config.settings + // https://github.com/mongodb/mongo/blob/0c265adbde984c981946f804279693078e0b9f8a/src/mongo/db/global_catalog/ddl/sharding_catalog_manager.cpp#L558-L559 + // https://github.com/mongodb/mongo/blob/0c265adbde984c981946f804279693078e0b9f8a/src/mongo/s/balancer_configuration.cpp#L122-L143 + return ( + l.id === 3216000 && + ['ReplWriterWorker', 'OplogApplier'].some((match) => + l.context.includes(match), + ) + ); + }, + (l: LogEntry) => { + // "Deprecated operation requested" for OP_QUERY which drivers may + // still send in limited situations until NODE-6287 is done + return l.id === 5578800 && l.attr?.op === 'query'; + }, + ]; + + private collectedWarnings: LogEntry[] = []; + private warningFilters: ((entry: LogEntry) => boolean)[] = []; + private cluster: MongoLogEventEmitter; + + constructor(cluster: MongoLogEventEmitter) { + this.cluster = cluster; + // Add default warning filters + for (const filter of ServerLogsChecker.defaultAllowedWarnings) { + this.allowWarning(filter); + } + + // Subscribe to mongoLog events + this.cluster.on('mongoLog', this.listener); + } + + private listener: (serverUUID: string, entry: LogEntry) => void = ( + _serverUUID: string, + entry: LogEntry, + ) => { + // Only collect warnings (W), errors (E), and fatal (F) severity logs + // Apply filters at collection time - filtered warnings are never stored + if ( + (entry.severity === 'W' || + entry.severity === 'E' || + entry.severity === 'F') && + !this.warningFilters.some((filter) => filter(entry)) + ) { + this.collectedWarnings.push(entry); + } + }; + + /** + * Get a copy of the collected warnings. + */ + get warnings(): LogEntry[] { + return [...this.collectedWarnings]; + } + + /** + * Allow a specific warning to pass validation. + * Must be called BEFORE the warning occurs (filters are applied at collection time). + * @param filter - A log ID (number) or predicate function + * @returns A function to unsubscribe this filter + */ + allowWarning(filter: WarningFilter): () => void { + const filterFn = + typeof filter === 'number' + ? (entry: LogEntry) => entry.id === filter + : filter; + + this.warningFilters.push(filterFn); + return () => { + const index = this.warningFilters.indexOf(filterFn); + if (index !== -1) { + this.warningFilters.splice(index, 1); + } + }; + } + + /** + * Check for unexpected warnings and throw if any are found. + * Clears the collected warnings after checking. + */ + noServerWarningsCheckpoint(): void { + const warnings = this.warnings; + this.collectedWarnings = []; + + if (warnings.length > 0) { + const warningDetails = warnings + .map((w) => ` - [${w.severity}] ID:${w.id ?? 'unknown'} ${w.message}`) + .join('\n'); + throw new Error( + `Unexpected server warnings detected:\n${warningDetails}`, + ); + } + } + + /** + * Stop listening to log events. + */ + close(): void { + this.cluster.off('mongoLog', this.listener); + } +} diff --git a/packages/mongodb-server-log-checker/tsconfig-lint.json b/packages/mongodb-server-log-checker/tsconfig-lint.json new file mode 100644 index 00000000..6bdef84f --- /dev/null +++ b/packages/mongodb-server-log-checker/tsconfig-lint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/mongodb-server-log-checker/tsconfig.json b/packages/mongodb-server-log-checker/tsconfig.json new file mode 100644 index 00000000..4e6d1b8b --- /dev/null +++ b/packages/mongodb-server-log-checker/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@mongodb-js/tsconfig-devtools/tsconfig.common.json" +}