diff --git a/packages/backend/test/unit/telemetry-database-instrumentation.ts b/packages/backend/test/unit/telemetry-database-instrumentation.ts index 20b3789a26..eeb3b4e039 100644 --- a/packages/backend/test/unit/telemetry-database-instrumentation.ts +++ b/packages/backend/test/unit/telemetry-database-instrumentation.ts @@ -3,9 +3,15 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { createRequire } from 'node:module'; +import { context, propagation, trace } from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { describe, expect, test, vi } from 'vitest'; +import type { Span as SdkSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { installDatabaseInstrumentation, installInstrumentation } from '@/core/telemetry/database-instrumentation.js'; +const backendRequire = createRequire(import.meta.url); + describe('database-instrumentation', () => { test('does not install PostgreSQL instrumentation when disabled', async () => { const uninstall = await installDatabaseInstrumentation({} as any, { @@ -55,6 +61,76 @@ describe('database-instrumentation', () => { expect(pg.disable).toHaveBeenCalledOnce(); }); + test('redacts SQL attributes after pg instrumentation records the statement', async () => { + const statement = 'SELECT very_secret_literal'; + const attributeWrites: Array<{ key: string; value: unknown }> = []; + let attributesAtStart: Record | undefined; + let pgSpan: SdkSpan | undefined; + const spanProcessor: SpanProcessor = { + forceFlush: async () => {}, + onStart: span => { + if (span.instrumentationScope.name !== '@opentelemetry/instrumentation-pg') return; + + pgSpan = span; + attributesAtStart = { ...span.attributes }; + const setAttribute = span.setAttribute.bind(span); + span.setAttribute = (key, value) => { + attributeWrites.push({ key, value }); + return setAttribute(key, value); + }; + }, + onEnd: () => {}, + shutdown: async () => {}, + }; + const provider = new NodeTracerProvider({ spanProcessors: [spanProcessor] }); + provider.register(); + let uninstall: (() => void) | undefined; + + try { + uninstall = await installDatabaseInstrumentation(provider, { + capturePgSpans: true, + capturePgStatement: false, + capturePgConnectionSpans: false, + }); + const { Client } = backendRequire('pg') as typeof import('pg'); + const tracer = provider.getTracer('database-instrumentation-test'); + + tracer.startActiveSpan('parent', parentSpan => { + try { + // pg instrumentation runs before an unconnected client queues the query, + // so no database is required. + new Client().query(statement, () => {}); + } finally { + parentSpan.end(); + } + }); + + const recordedPgSpan = pgSpan; + expect(recordedPgSpan).toBeDefined(); + if (recordedPgSpan == null) throw new Error('PostgreSQL span was not started.'); + expect(attributesAtStart).not.toHaveProperty('db.statement'); + expect(attributesAtStart).not.toHaveProperty('db.query.text'); + + const rawSqlIndex = attributeWrites.findIndex(({ key, value }) => + (key === 'db.statement' || key === 'db.query.text') && value === statement); + expect(rawSqlIndex).toBeGreaterThanOrEqual(0); + const rawSqlKey = attributeWrites[rawSqlIndex].key; + expect(attributeWrites.findIndex(({ key, value }, index) => + index > rawSqlIndex && key === rawSqlKey && value === '[REDACTED]')).toBeGreaterThan(rawSqlIndex); + + expect(recordedPgSpan.attributes['db.statement']).toBe('[REDACTED]'); + expect(recordedPgSpan.attributes['db.query.text']).toBe('[REDACTED]'); + expect(Object.values(recordedPgSpan.attributes)).not.toContain(statement); + } finally { + pgSpan?.end(); + uninstall?.(); + await provider.shutdown(); + trace.disable(); + context.disable(); + propagation.disable(); + } + }); + test('keeps SQL statement attributes when explicitly enabled', () => { const pg = { setTracerProvider: vi.fn(), enable: vi.fn(), disable: vi.fn() }; const config = vi.fn();