Skip to content

Commit c1bcf0d

Browse files
W-A-Jamesnbbeeken
andauthored
perf(NODE-6616): shortcircuit logging ejson.stringify (#4377)
Co-authored-by: Neal Beeken <[email protected]>
1 parent fd902d3 commit c1bcf0d

File tree

2 files changed

+251
-3
lines changed

2 files changed

+251
-3
lines changed

src/mongo_logger.ts

+128-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
import { inspect, promisify } from 'util';
2+
import { isUint8Array } from 'util/types';
23

3-
import { type Document, EJSON, type EJSONOptions, type ObjectId } from './bson';
4+
import {
5+
type Binary,
6+
type BSONRegExp,
7+
type BSONSymbol,
8+
type Code,
9+
type DBRef,
10+
type Decimal128,
11+
type Document,
12+
type Double,
13+
EJSON,
14+
type EJSONOptions,
15+
type Int32,
16+
type Long,
17+
type MaxKey,
18+
type MinKey,
19+
type ObjectId,
20+
type Timestamp
21+
} from './bson';
422
import type { CommandStartedEvent } from './cmap/command_monitoring_events';
523
import type {
624
ConnectionCheckedInEvent,
@@ -413,6 +431,20 @@ export interface LogConvertible extends Record<string, any> {
413431
toLog(): Record<string, any>;
414432
}
415433

434+
type BSONObject =
435+
| BSONRegExp
436+
| BSONSymbol
437+
| Code
438+
| DBRef
439+
| Decimal128
440+
| Double
441+
| Int32
442+
| Long
443+
| MaxKey
444+
| MinKey
445+
| ObjectId
446+
| Timestamp
447+
| Binary;
416448
/** @internal */
417449
export function stringifyWithMaxLen(
418450
value: any,
@@ -421,13 +453,107 @@ export function stringifyWithMaxLen(
421453
): string {
422454
let strToTruncate = '';
423455

456+
let currentLength = 0;
457+
const maxDocumentLengthEnsurer = function maxDocumentLengthEnsurer(key: string, value: any) {
458+
if (currentLength >= maxDocumentLength) {
459+
return undefined;
460+
}
461+
// Account for root document
462+
if (key === '') {
463+
// Account for starting brace
464+
currentLength += 1;
465+
return value;
466+
}
467+
468+
// +4 accounts for 2 quotation marks, colon and comma after value
469+
// Note that this potentially undercounts since it does not account for escape sequences which
470+
// will have an additional backslash added to them once passed through JSON.stringify.
471+
currentLength += key.length + 4;
472+
473+
if (value == null) return value;
474+
475+
switch (typeof value) {
476+
case 'string':
477+
// +2 accounts for quotes
478+
// Note that this potentially undercounts similarly to the key length calculation
479+
currentLength += value.length + 2;
480+
break;
481+
case 'number':
482+
case 'bigint':
483+
currentLength += String(value).length;
484+
break;
485+
case 'boolean':
486+
currentLength += value ? 4 : 5;
487+
break;
488+
case 'object':
489+
if (isUint8Array(value)) {
490+
// '{"$binary":{"base64":"<base64 string>","subType":"XX"}}'
491+
// This is an estimate based on the fact that the base64 is approximately 1.33x the length of
492+
// the actual binary sequence https://en.wikipedia.org/wiki/Base64
493+
currentLength += (22 + value.byteLength + value.byteLength * 0.33 + 18) | 0;
494+
} else if ('_bsontype' in value) {
495+
const v = value as BSONObject;
496+
switch (v._bsontype) {
497+
case 'Int32':
498+
currentLength += String(v.value).length;
499+
break;
500+
case 'Double':
501+
// Account for representing integers as <value>.0
502+
currentLength +=
503+
(v.value | 0) === v.value ? String(v.value).length + 2 : String(v.value).length;
504+
break;
505+
case 'Long':
506+
currentLength += v.toString().length;
507+
break;
508+
case 'ObjectId':
509+
// '{"$oid":"XXXXXXXXXXXXXXXXXXXXXXXX"}'
510+
currentLength += 35;
511+
break;
512+
case 'MaxKey':
513+
case 'MinKey':
514+
// '{"$maxKey":1}' or '{"$minKey":1}'
515+
currentLength += 13;
516+
break;
517+
case 'Binary':
518+
// '{"$binary":{"base64":"<base64 string>","subType":"XX"}}'
519+
// This is an estimate based on the fact that the base64 is approximately 1.33x the length of
520+
// the actual binary sequence https://en.wikipedia.org/wiki/Base64
521+
currentLength += (22 + value.position + value.position * 0.33 + 18) | 0;
522+
break;
523+
case 'Timestamp':
524+
// '{"$timestamp":{"t":<t>,"i":<i>}}'
525+
currentLength += 19 + String(v.t).length + 5 + String(v.i).length + 2;
526+
break;
527+
case 'Code':
528+
// '{"$code":"<code>"}' or '{"$code":"<code>","$scope":<scope>}'
529+
if (v.scope == null) {
530+
currentLength += v.code.length + 10 + 2;
531+
} else {
532+
// Ignoring actual scope object, so this undercounts by a significant amount
533+
currentLength += v.code.length + 10 + 11;
534+
}
535+
break;
536+
case 'BSONRegExp':
537+
// '{"$regularExpression":{"pattern":"<pattern>","options":"<options>"}}'
538+
currentLength += 34 + v.pattern.length + 13 + v.options.length + 3;
539+
break;
540+
}
541+
}
542+
}
543+
return value;
544+
};
545+
424546
if (typeof value === 'string') {
425547
strToTruncate = value;
426548
} else if (typeof value === 'function') {
427549
strToTruncate = value.name;
428550
} else {
429551
try {
430-
strToTruncate = EJSON.stringify(value, options);
552+
if (maxDocumentLength !== 0) {
553+
strToTruncate = EJSON.stringify(value, maxDocumentLengthEnsurer, 0, options);
554+
} else {
555+
strToTruncate = EJSON.stringify(value, options);
556+
}
431557
} catch (e) {
432558
strToTruncate = `Extended JSON serialization failed with: ${e.message}`;
433559
}

test/unit/mongo_logger.test.ts

+123-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EJSON, ObjectId } from 'bson';
1+
import { BSONRegExp, Code, Double, EJSON, Int32, MaxKey, MinKey, ObjectId } from 'bson';
22
import { expect } from 'chai';
33
import * as sinon from 'sinon';
44
import { Readable, Writable } from 'stream';
@@ -1612,3 +1612,125 @@ describe('class MongoLogger', function () {
16121612
}
16131613
});
16141614
});
1615+
1616+
describe('stringifyWithMaxLen', function () {
1617+
describe('when stringifying a string field', function () {
1618+
it('does not prematurely redact the next key', function () {
1619+
const doc = {
1620+
a: 'aaa',
1621+
b: 'bbb'
1622+
};
1623+
1624+
const returnVal = stringifyWithMaxLen(doc, 13);
1625+
expect(returnVal).to.contain('"b...');
1626+
});
1627+
});
1628+
1629+
describe('when stringifying a number field', function () {
1630+
it('does not prematurely redact the next key', function () {
1631+
const doc = {
1632+
a: 1000,
1633+
b: 'bbb'
1634+
};
1635+
const returnVal = stringifyWithMaxLen(doc, 12);
1636+
1637+
expect(returnVal).to.contain('"b...');
1638+
});
1639+
});
1640+
1641+
describe('when stringifying a bigint field', function () {
1642+
it('does not prematurely redact the next key', function () {
1643+
const doc = {
1644+
a: 1000n,
1645+
b: 'bbb'
1646+
};
1647+
const returnVal = stringifyWithMaxLen(doc, 12);
1648+
1649+
expect(returnVal).to.contain('"b...');
1650+
});
1651+
});
1652+
1653+
describe('when stringifying a BSON Code field', function () {
1654+
it('does not prematurely redact the next key', function () {
1655+
const doc = {
1656+
c: new Code('console.log();'),
1657+
b: 'bbb'
1658+
};
1659+
const returnVal = stringifyWithMaxLen(doc, 34);
1660+
1661+
expect(returnVal).to.contain('"b...');
1662+
});
1663+
});
1664+
1665+
describe('when stringifying a BSON Double field', function () {
1666+
it('does not prematurely redact the next key', function () {
1667+
const doc = {
1668+
c: new Double(123.1),
1669+
b: 'bbb'
1670+
};
1671+
const returnVal = stringifyWithMaxLen(doc, 13);
1672+
1673+
expect(returnVal).to.contain('"b...');
1674+
});
1675+
});
1676+
1677+
describe('when stringifying a BSON Int32 field', function () {
1678+
it('does not prematurely redact the next key', function () {
1679+
const doc = {
1680+
c: new Int32(123),
1681+
b: 'bbb'
1682+
};
1683+
const returnVal = stringifyWithMaxLen(doc, 11);
1684+
1685+
expect(returnVal).to.contain('"b...');
1686+
});
1687+
});
1688+
1689+
describe('when stringifying a BSON MaxKey field', function () {
1690+
it('does not prematurely redact the next key', function () {
1691+
const doc = {
1692+
c: new MaxKey(),
1693+
b: 'bbb'
1694+
};
1695+
const returnVal = stringifyWithMaxLen(doc, 21);
1696+
1697+
expect(returnVal).to.contain('"b...');
1698+
});
1699+
});
1700+
1701+
describe('when stringifying a BSON MinKey field', function () {
1702+
it('does not prematurely redact the next key', function () {
1703+
const doc = {
1704+
c: new MinKey(),
1705+
b: 'bbb'
1706+
};
1707+
const returnVal = stringifyWithMaxLen(doc, 21);
1708+
1709+
expect(returnVal).to.contain('"b...');
1710+
});
1711+
});
1712+
1713+
describe('when stringifying a BSON ObjectId field', function () {
1714+
it('does not prematurely redact the next key', function () {
1715+
const doc = {
1716+
c: new ObjectId(),
1717+
b: 'bbb'
1718+
};
1719+
const returnVal = stringifyWithMaxLen(doc, 43);
1720+
1721+
expect(returnVal).to.contain('"b...');
1722+
});
1723+
});
1724+
1725+
describe('when stringifying a BSON BSONRegExp field', function () {
1726+
it('does not prematurely redact the next key', function () {
1727+
const doc = {
1728+
c: new BSONRegExp('testRegex', 'is'),
1729+
b: 'bbb'
1730+
};
1731+
const returnVal = stringifyWithMaxLen(doc, 69);
1732+
1733+
expect(returnVal).to.contain('"b...');
1734+
});
1735+
});
1736+
});

0 commit comments

Comments
 (0)