From 6d1a251ebc836accbedf2c408f11200674f65714 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 12 Nov 2024 14:36:45 -0500 Subject: [PATCH] fix(mongo): rewrite Buffer as ? during serialization (#14071) --- .../suites/tracing/mongodb/test.ts | 12 ++-- .../node/src/integrations/tracing/mongo.ts | 46 ++++++++++++ .../test/integrations/tracing/mongo.test.ts | 72 +++++++++++++++++++ 3 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 packages/node/test/integrations/tracing/mongo.test.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts index 59c50d32ebdc..92fc857ed4e8 100644 --- a/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/mongodb/test.ts @@ -71,12 +71,10 @@ describe('MongoDB experimental Test', () => { 'db.connection_string': expect.any(String), 'net.peer.name': expect.any(String), 'net.peer.port': expect.any(Number), - 'db.statement': - '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}', + 'db.statement': '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', 'otel.kind': 'CLIENT', }, - description: - '{"title":"?","_id":{"_bsontype":"?","id":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?"}}}', + description: '{"title":"?","_id":{"_bsontype":"?","id":"?"}}', op: 'db', origin: 'auto.db.otel.mongo', }), @@ -162,12 +160,10 @@ describe('MongoDB experimental Test', () => { 'db.connection_string': expect.any(String), 'net.peer.name': expect.any(String), 'net.peer.port': expect.any(Number), - 'db.statement': - '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}', + 'db.statement': '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', 'otel.kind': 'CLIENT', }, - description: - '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":{"0":"?","1":"?","2":"?","3":"?","4":"?","5":"?","6":"?","7":"?","8":"?","9":"?","10":"?","11":"?","12":"?","13":"?","14":"?","15":"?"}}}]}', + description: '{"endSessions":[{"id":{"_bsontype":"?","sub_type":"?","position":"?","buffer":"?"}}]}', op: 'db', origin: 'auto.db.otel.mongo', }), diff --git a/packages/node/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts index 5e42f5611db8..5f4d4e66a8a6 100644 --- a/packages/node/src/integrations/tracing/mongo.ts +++ b/packages/node/src/integrations/tracing/mongo.ts @@ -11,12 +11,58 @@ export const instrumentMongo = generateInstrumentOnce( INTEGRATION_NAME, () => new MongoDBInstrumentation({ + dbStatementSerializer: _defaultDbStatementSerializer, responseHook(span) { addOriginToSpan(span, 'auto.db.otel.mongo'); }, }), ); +/** + * Replaces values in document with '?', hiding PII and helping grouping. + */ +export function _defaultDbStatementSerializer(commandObj: Record): string { + const resultObj = _scrubStatement(commandObj); + return JSON.stringify(resultObj); +} + +function _scrubStatement(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(element => _scrubStatement(element)); + } + + if (isCommandObj(value)) { + const initial: Record = {}; + return Object.entries(value) + .map(([key, element]) => [key, _scrubStatement(element)]) + .reduce((prev, current) => { + if (isCommandEntry(current)) { + prev[current[0]] = current[1]; + } + return prev; + }, initial); + } + + // A value like string or number, possible contains PII, scrub it + return '?'; +} + +function isCommandObj(value: Record | unknown): value is Record { + return typeof value === 'object' && value !== null && !isBuffer(value); +} + +function isBuffer(value: unknown): boolean { + let isBuffer = false; + if (typeof Buffer !== 'undefined') { + isBuffer = Buffer.isBuffer(value); + } + return isBuffer; +} + +function isCommandEntry(value: [string, unknown] | unknown): value is [string, unknown] { + return Array.isArray(value); +} + const _mongoIntegration = (() => { return { name: INTEGRATION_NAME, diff --git a/packages/node/test/integrations/tracing/mongo.test.ts b/packages/node/test/integrations/tracing/mongo.test.ts new file mode 100644 index 000000000000..29571c07babe --- /dev/null +++ b/packages/node/test/integrations/tracing/mongo.test.ts @@ -0,0 +1,72 @@ +import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; + +import { + _defaultDbStatementSerializer, + instrumentMongo, + mongoIntegration, +} from '../../../src/integrations/tracing/mongo'; +import { INSTRUMENTED } from '../../../src/otel/instrument'; + +jest.mock('@opentelemetry/instrumentation-mongodb'); + +describe('Mongo', () => { + beforeEach(() => { + jest.clearAllMocks(); + delete INSTRUMENTED.Mongo; + + (MongoDBInstrumentation as unknown as jest.SpyInstance).mockImplementation(() => { + return { + setTracerProvider: () => undefined, + setMeterProvider: () => undefined, + getConfig: () => ({}), + setConfig: () => ({}), + enable: () => undefined, + }; + }); + }); + + it('defaults are correct for instrumentMongo', () => { + instrumentMongo(); + + expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); + expect(MongoDBInstrumentation).toHaveBeenCalledWith({ + dbStatementSerializer: expect.any(Function), + responseHook: expect.any(Function), + }); + }); + + it('defaults are correct for mongoIntegration', () => { + mongoIntegration().setupOnce!(); + + expect(MongoDBInstrumentation).toHaveBeenCalledTimes(1); + expect(MongoDBInstrumentation).toHaveBeenCalledWith({ + responseHook: expect.any(Function), + dbStatementSerializer: expect.any(Function), + }); + }); + + describe('_defaultDbStatementSerializer', () => { + it('rewrites strings as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: 'foo', + }); + expect(JSON.parse(serialized).find).toBe('?'); + }); + + it('rewrites nested strings as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: { + inner: 'foo', + }, + }); + expect(JSON.parse(serialized).find.inner).toBe('?'); + }); + + it('rewrites Buffer as ?', () => { + const serialized = _defaultDbStatementSerializer({ + find: Buffer.from('foo', 'utf8'), + }); + expect(JSON.parse(serialized).find).toBe('?'); + }); + }); +});