opentelemetry-js/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/test/pg.test.ts

440 lines
13 KiB
TypeScript

/*!
* Copyright 2019, OpenTelemetry Authors
*
* 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
*
* https://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.
*/
import {
Attributes,
CanonicalCode,
context,
Span,
SpanKind,
Status,
TimedEvent,
} from '@opentelemetry/api';
import { NoopLogger } from '@opentelemetry/core';
import { NodeTracerProvider } from '@opentelemetry/node';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import * as testUtils from '@opentelemetry/test-utils';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/tracing';
import * as assert from 'assert';
import * as pg from 'pg';
import { plugin, PostgresPlugin } from '../src';
import { AttributeNames } from '../src/enums';
const memoryExporter = new InMemorySpanExporter();
const CONFIG = {
user: process.env.POSTGRES_USER || 'postgres',
database: process.env.POSTGRES_DB || 'postgres',
host: process.env.POSTGRES_HOST || 'localhost',
port: process.env.POSTGRES_PORT
? parseInt(process.env.POSTGRES_PORT, 10)
: 54320,
};
const DEFAULT_ATTRIBUTES = {
[AttributeNames.COMPONENT]: PostgresPlugin.COMPONENT,
[AttributeNames.DB_INSTANCE]: CONFIG.database,
[AttributeNames.DB_TYPE]: PostgresPlugin.DB_TYPE,
[AttributeNames.PEER_HOSTNAME]: CONFIG.host,
[AttributeNames.PEER_ADDRESS]: `jdbc:postgresql://${CONFIG.host}:${CONFIG.port}/${CONFIG.database}`,
[AttributeNames.PEER_PORT]: CONFIG.port,
[AttributeNames.DB_USER]: CONFIG.user,
};
const okStatus: Status = {
code: CanonicalCode.OK,
};
const unknownStatus: Status = {
code: CanonicalCode.UNKNOWN,
};
const runCallbackTest = (
span: Span | null,
attributes: Attributes,
events: TimedEvent[],
status: Status = okStatus,
spansLength = 1,
spansIndex = 0
) => {
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, spansLength);
const pgSpan = spans[spansIndex];
testUtils.assertSpan(pgSpan, SpanKind.CLIENT, attributes, events, status);
if (span) {
testUtils.assertPropagation(pgSpan, span);
}
};
describe('pg@7.x', () => {
let client: pg.Client;
let contextManager: AsyncHooksContextManager;
const provider = new NodeTracerProvider();
const tracer = provider.getTracer('external');
const logger = new NoopLogger();
const testPostgres = process.env.RUN_POSTGRES_TESTS; // For CI: assumes local postgres db is already available
const testPostgresLocally = process.env.RUN_POSTGRES_TESTS_LOCAL; // For local: spins up local postgres db via docker
const shouldTest = testPostgres || testPostgresLocally; // Skips these tests if false (default)
before(async function() {
if (!shouldTest) {
// this.skip() workaround
// https://github.com/mochajs/mocha/issues/2683#issuecomment-375629901
this.test!.parent!.pending = true;
this.skip();
}
provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
if (testPostgresLocally) {
testUtils.startDocker('postgres');
}
client = new pg.Client(CONFIG);
try {
await client.connect();
} catch (e) {
throw e;
}
});
after(async () => {
if (testPostgresLocally) {
testUtils.cleanUpDocker('postgres');
}
await client.end();
});
beforeEach(function() {
plugin.enable(pg, provider, logger);
contextManager = new AsyncHooksContextManager().enable();
context.setGlobalContextManager(contextManager);
});
afterEach(() => {
memoryExporter.reset();
plugin.disable();
contextManager.disable();
});
it('should return a plugin', () => {
assert.ok(plugin instanceof PostgresPlugin);
});
it('should have correct moduleName', () => {
assert.strictEqual(plugin.moduleName, 'pg');
});
it('should maintain pg module error throwing behavior with bad arguments', () => {
const assertPgError = (e: Error) => {
const src = e.stack!.split('\n').map(line => line.trim())[1];
return /node_modules[/\\]pg/.test(src);
};
assert.throws(
() => {
(client as any).query();
},
assertPgError,
'pg should throw when no args provided'
);
runCallbackTest(null, DEFAULT_ATTRIBUTES, [], unknownStatus);
memoryExporter.reset();
assert.doesNotThrow(
() =>
(client as any).query({ foo: 'bar' }, undefined, () => {
runCallbackTest(
null,
{
...DEFAULT_ATTRIBUTES,
},
[],
unknownStatus
);
}),
'pg should not throw when invalid config args are provided'
);
});
describe('#client.query(...)', () => {
it('should not return a promise if callback is provided', done => {
const res = client.query('SELECT NOW()', (err, res) => {
assert.strictEqual(err, null);
done();
});
assert.strictEqual(res, undefined, 'No promise is returned');
});
it('should return a promise if callback is provided', done => {
const resPromise = client.query('SELECT NOW()');
resPromise
.then(res => {
assert.ok(res);
done();
})
.catch((err: Error) => {
assert.ok(false, err.message);
});
});
it('should intercept client.query(text, callback)', done => {
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: 'SELECT NOW()',
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
tracer.withSpan(span, () => {
const res = client.query('SELECT NOW()', (err, res) => {
assert.strictEqual(err, null);
assert.ok(res);
runCallbackTest(span, attributes, events);
done();
});
assert.strictEqual(res, undefined, 'No promise is returned');
});
});
it('should intercept client.query(text, values, callback)', done => {
const query = 'SELECT $1::text';
const values = ['0'];
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: query,
[AttributeNames.PG_VALUES]: '[0]',
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
tracer.withSpan(span, () => {
const resNoPromise = client.query(query, values, (err, res) => {
assert.strictEqual(err, null);
assert.ok(res);
runCallbackTest(span, attributes, events);
done();
});
assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
});
});
it('should intercept client.query({text, callback})', done => {
const query = 'SELECT NOW()';
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: query,
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
tracer.withSpan(span, () => {
const resNoPromise = client.query({
text: query,
callback: (err: Error, res: pg.QueryResult) => {
assert.strictEqual(err, null);
assert.ok(res);
runCallbackTest(span, attributes, events);
done();
},
} as pg.QueryConfig);
assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
});
});
it('should intercept client.query({text}, callback)', done => {
const query = 'SELECT NOW()';
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: query,
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
tracer.withSpan(span, () => {
const resNoPromise = client.query({ text: query }, (err, res) => {
assert.strictEqual(err, null);
assert.ok(res);
runCallbackTest(span, attributes, events);
done();
});
assert.strictEqual(resNoPromise, undefined, 'No promise is returned');
});
});
it('should intercept client.query(text, values)', async () => {
const query = 'SELECT $1::text';
const values = ['0'];
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: query,
[AttributeNames.PG_VALUES]: '[0]',
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
await tracer.withSpan(span, async () => {
const resPromise = await client.query(query, values);
try {
assert.ok(resPromise);
runCallbackTest(span, attributes, events);
} catch (e) {
assert.ok(false, e.message);
}
});
});
it('should intercept client.query({text, values})', async () => {
const query = 'SELECT $1::text';
const values = ['0'];
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: query,
[AttributeNames.PG_VALUES]: '[0]',
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
await tracer.withSpan(span, async () => {
const resPromise = await client.query({
text: query,
values: values,
});
try {
assert.ok(resPromise);
runCallbackTest(span, attributes, events);
} catch (e) {
assert.ok(false, e.message);
}
});
});
it('should intercept client.query(plan)', async () => {
const name = 'fetch-text';
const query = 'SELECT $1::text';
const values = ['0'];
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.PG_PLAN]: name,
[AttributeNames.DB_STATEMENT]: query,
[AttributeNames.PG_VALUES]: '[0]',
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
await tracer.withSpan(span, async () => {
try {
const resPromise = await client.query({
name: name,
text: query,
values: values,
});
assert.strictEqual(resPromise.command, 'SELECT');
runCallbackTest(span, attributes, events);
} catch (e) {
assert.ok(false, e.message);
}
});
});
it('should intercept client.query(text)', async () => {
const query = 'SELECT NOW()';
const attributes = {
...DEFAULT_ATTRIBUTES,
[AttributeNames.DB_STATEMENT]: query,
};
const events: TimedEvent[] = [];
const span = tracer.startSpan('test span');
await tracer.withSpan(span, async () => {
try {
const resPromise = await client.query(query);
assert.ok(resPromise);
runCallbackTest(span, attributes, events);
} catch (e) {
assert.ok(false, e.message);
}
});
});
it('should handle the same callback being given to multiple client.query()s', done => {
let events = 0;
const parent = tracer.startSpan('parent');
const queryHandler = (err?: Error, res?: pg.QueryResult) => {
const span = tracer.getCurrentSpan();
assert.deepStrictEqual(span!.context(), parent.context());
if (err) {
throw err;
}
events += 1;
if (events === 7) {
done();
}
};
const config = {
text: 'SELECT NOW()',
callback: queryHandler,
};
tracer.withSpan(parent, () => {
client.query(config.text, config.callback); // 1
client.query(config); // 2
client.query(config.text, queryHandler); // 3
client.query(config.text, queryHandler); // 4
client
.query(config.text)
.then(result => queryHandler(undefined, result))
.catch(err => queryHandler(err)); // 5
client.query(config); // 6
client.query(config); // 7
});
});
it('should preserve correct context even when using the same callback in client.query()', done => {
const spans = [tracer.startSpan('span 1'), tracer.startSpan('span 2')];
const currentSpans: (Span | undefined)[] = [];
const queryHandler = () => {
currentSpans.push(tracer.getCurrentSpan());
if (currentSpans.length === 2) {
assert.deepStrictEqual(currentSpans, spans);
done();
}
};
tracer.withSpan(spans[0], () => {
client.query('SELECT NOW()', queryHandler);
});
tracer.withSpan(spans[1], () => {
client.query('SELECT NOW()', queryHandler);
});
});
it('should preserve correct context even when using the same promise resolver in client.query()', done => {
const spans = [tracer.startSpan('span 1'), tracer.startSpan('span 2')];
const currentSpans: (Span | undefined)[] = [];
const queryHandler = () => {
currentSpans.push(tracer.getCurrentSpan());
if (currentSpans.length === 2) {
assert.deepStrictEqual(currentSpans, spans);
done();
}
};
tracer.withSpan(spans[0], () => {
client.query('SELECT NOW()').then(queryHandler);
});
tracer.withSpan(spans[1], () => {
client.query('SELECT NOW()').then(queryHandler);
});
});
});
});