430 lines
13 KiB
TypeScript
430 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 { NoopLogger } from '@opentelemetry/core';
|
|
import { NodeTracerRegistry } from '@opentelemetry/node';
|
|
import {
|
|
InMemorySpanExporter,
|
|
SimpleSpanProcessor,
|
|
} from '@opentelemetry/tracing';
|
|
import {
|
|
SpanKind,
|
|
Attributes,
|
|
TimedEvent,
|
|
Span,
|
|
CanonicalCode,
|
|
Status,
|
|
} from '@opentelemetry/types';
|
|
import { plugin, PostgresPlugin } from '../src';
|
|
import { AttributeNames } from '../src/enums';
|
|
import * as assert from 'assert';
|
|
import * as pg from 'pg';
|
|
import * as testUtils from '@opentelemetry/test-utils';
|
|
|
|
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;
|
|
const registry = new NodeTracerRegistry();
|
|
const tracer = registry.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();
|
|
}
|
|
registry.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, registry, logger);
|
|
});
|
|
|
|
afterEach(() => {
|
|
memoryExporter.reset();
|
|
plugin.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 queryHandler = (err: Error, res: pg.QueryResult) => {
|
|
const span = tracer.getCurrentSpan();
|
|
assert.ok(span);
|
|
assert.strictEqual((span as any)['_ended'], false);
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
events += 1;
|
|
};
|
|
|
|
const config = {
|
|
text: 'SELECT NOW()',
|
|
callback: queryHandler,
|
|
};
|
|
|
|
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); // Not using queryHandler
|
|
client.query(config); // 5
|
|
client.query(config); // 6
|
|
client.query(config.text, (err, res) => {
|
|
assert.strictEqual(events, 6);
|
|
done();
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
});
|