Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit 29bb15c

Browse files
authored
feat: support context propagation in bluebird (#872)
1 parent 990bfe0 commit 29bb15c

9 files changed

Lines changed: 241 additions & 72 deletions

File tree

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,14 @@ export const defaultConfig = {
255255
maximumLabelValueSize: 512,
256256
plugins: {
257257
// enable all by default
258+
'bluebird': path.join(pluginDirectory, 'plugin-bluebird.js'),
258259
'connect': path.join(pluginDirectory, 'plugin-connect.js'),
259260
'express': path.join(pluginDirectory, 'plugin-express.js'),
260261
'generic-pool': path.join(pluginDirectory, 'plugin-generic-pool.js'),
261262
'grpc': path.join(pluginDirectory, 'plugin-grpc.js'),
262263
'hapi': path.join(pluginDirectory, 'plugin-hapi.js'),
263264
'http': path.join(pluginDirectory, 'plugin-http.js'),
264265
'http2': path.join(pluginDirectory, 'plugin-http2.js'),
265-
'knex': path.join(pluginDirectory, 'plugin-knex.js'),
266266
'koa': path.join(pluginDirectory, 'plugin-koa.js'),
267267
'mongodb-core': path.join(pluginDirectory, 'plugin-mongodb-core.js'),
268268
'mysql': path.join(pluginDirectory, 'plugin-mysql.js'),

src/plugins/plugin-bluebird.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright 2018 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as shimmer from 'shimmer';
18+
19+
import {PluginTypes} from '..';
20+
21+
import {bluebird_3} from './types';
22+
23+
type BluebirdModule = typeof bluebird_3&{prototype: {_then: Function;}};
24+
25+
const plugin: PluginTypes.Plugin = [{
26+
// Bluebird is a class.
27+
// tslint:disable-next-line:variable-name
28+
patch: (Bluebird, tracer) => {
29+
// any is a type arg; args are type checked when read directly, otherwise
30+
// passed through to a function with the same type signature.
31+
// tslint:disable:no-any
32+
const wrapIfFunction = (fn: any) =>
33+
typeof fn === 'function' ? tracer.wrap(fn) : fn;
34+
shimmer.wrap(Bluebird.prototype, '_then', (thenFn: Function) => {
35+
// Inherit context from the call site of .then().
36+
return function<T>(this: bluebird_3<T>, ...args: any[]) {
37+
return thenFn.apply(this, [
38+
wrapIfFunction(args[0]), wrapIfFunction(args[1]), ...args.slice(2)
39+
]);
40+
};
41+
});
42+
// tslint:enable:no-any
43+
}
44+
} as PluginTypes.Monkeypatch<BluebirdModule>];
45+
46+
export = plugin;

src/plugins/plugin-knex.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

src/plugins/types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* contain dots.
2828
*/
2929

30+
import * as bluebird_3 from './bluebird_3'; // bluebird@3
3031
import * as connect_3 from './connect_3'; // connect@3
3132
import * as express_4 from './express_4'; // express@4
3233
import * as hapi_16 from './hapi_16'; // hapi@16
@@ -86,6 +87,7 @@ declare namespace pg_6 {
8687
//---exports---//
8788

8889
export {
90+
bluebird_3,
8991
connect_3,
9092
express_4,
9193
hapi_16,

test/fixtures/plugin-fixtures.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
{
2+
"bluebird3": {
3+
"dependencies": {
4+
"bluebird": "^3.5.2"
5+
}
6+
},
27
"connect3": {
38
"dependencies": {
49
"connect": "^3.5.0"

test/plugins/test-cls-bluebird.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as assert from 'assert';
18+
19+
import {bluebird_3 as BluebirdPromise} from '../../src/plugins/types';
20+
import {Trace} from '../../src/trace';
21+
import * as traceTestModule from '../trace';
22+
23+
/**
24+
* Describes a test case.
25+
*/
26+
interface TestCase<T = void> {
27+
/**
28+
* Description of a test case; included in string argument to it().
29+
*/
30+
description: string;
31+
/**
32+
* Creates and returns a new Promise.
33+
*/
34+
makePromise: () => BluebirdPromise<T>;
35+
/**
36+
* Given a Promise and a callback, calls the callback some time after the
37+
* Promise has been resolved or rejected.
38+
*/
39+
thenFn: (promise: BluebirdPromise<T>, cb: () => void) => void;
40+
}
41+
/**
42+
* For a given Promise implementation, create two traces:
43+
* 1. Constructs a new Promise and resolves it.
44+
* 2. Within a then callback to the above mentioned Promise, construct a child
45+
* span.
46+
*/
47+
const getTracesForPromiseImplementation =
48+
<T>(makePromise: () => BluebirdPromise<T>,
49+
thenFn: (promise: BluebirdPromise<T>, cb: () => void) =>
50+
void): Promise<[Trace, Trace]> => {
51+
return new Promise((resolve, reject) => {
52+
const tracer = traceTestModule.get();
53+
let p: BluebirdPromise<T>;
54+
const firstSpan = tracer.runInRootSpan({name: 'first'}, span => {
55+
p = makePromise();
56+
return span;
57+
});
58+
tracer.runInRootSpan({name: 'second'}, secondSpan => {
59+
// Note to maintainers: Do NOT convert this to async/await,
60+
// as it changes context propagation behavior.
61+
thenFn(p, () => {
62+
tracer.createChildSpan().endSpan();
63+
secondSpan.endSpan();
64+
firstSpan.endSpan();
65+
setImmediate(() => {
66+
try {
67+
const trace1 = traceTestModule.getOneTrace(
68+
trace => trace.spans.some(root => root.name === 'first'));
69+
const trace2 = traceTestModule.getOneTrace(
70+
trace => trace.spans.some(root => root.name === 'second'));
71+
traceTestModule.clearTraceData();
72+
resolve([trace1, trace2]);
73+
} catch (e) {
74+
traceTestModule.clearTraceData();
75+
reject(e);
76+
}
77+
});
78+
});
79+
});
80+
});
81+
};
82+
83+
84+
describe('Patch plugin for bluebird', () => {
85+
// BPromise is a class.
86+
// tslint:disable-next-line:variable-name
87+
let BPromise: typeof BluebirdPromise;
88+
89+
before(() => {
90+
traceTestModule.setCLSForTest();
91+
traceTestModule.setPluginLoaderForTest();
92+
traceTestModule.start();
93+
BPromise = require('./fixtures/bluebird3');
94+
});
95+
96+
after(() => {
97+
traceTestModule.setCLSForTest(traceTestModule.TestCLS);
98+
traceTestModule.setPluginLoaderForTest(traceTestModule.TestPluginLoader);
99+
});
100+
101+
const testCases = [
102+
{
103+
description: 'immediate resolve + child from then callback',
104+
makePromise: () => new BPromise(res => res()),
105+
thenFn: (p, cb) => p.then(cb)
106+
} as TestCase,
107+
{
108+
description: 'deferred resolve + child from then callback',
109+
makePromise: () => new BPromise(res => setTimeout(res, 0)),
110+
thenFn: (p, cb) => p.then(cb)
111+
} as TestCase,
112+
{
113+
description: 'bound, deferred resolve + child from then callback',
114+
makePromise: () => new BPromise<void>(res => setTimeout(res, 0)).bind({}),
115+
thenFn: (p, cb) => p.then(cb)
116+
} as TestCase,
117+
{
118+
description: 'deferred resolve + child from spread callback',
119+
makePromise: () => new BPromise(res => setTimeout(() => res([]), 0)),
120+
thenFn: (p, cb) => p.spread(cb)
121+
} as TestCase<never[]>,
122+
{
123+
description: 'deferred rejection + child from then callback',
124+
makePromise: () => new BPromise((res, rej) => setTimeout(rej, 0)),
125+
thenFn: (p, cb) => p.then(null, cb)
126+
} as TestCase,
127+
{
128+
description: 'deferred rejection + child from catch callback',
129+
makePromise: () => new BPromise((res, rej) => setTimeout(rej, 0)),
130+
thenFn: (p, cb) => p.catch(cb)
131+
} as TestCase,
132+
{
133+
description: 'deferred rejection + child from error callback',
134+
makePromise: () => new BPromise(
135+
(res, rej) =>
136+
setTimeout(() => rej(new BPromise.OperationalError()), 0)),
137+
thenFn: (p, cb) => p.error(cb)
138+
} as TestCase,
139+
{
140+
description: 'deferred rejection + child from finally callback',
141+
makePromise: () => new BPromise((res, rej) => setTimeout(rej, 0)),
142+
thenFn: (p, cb) => p.catch(() => {}).finally(cb)
143+
} as TestCase,
144+
{
145+
description: 'immediate resolve + child after await',
146+
makePromise: () => new BPromise(res => res()),
147+
thenFn: async (p, cb) => {
148+
await p;
149+
cb();
150+
}
151+
} as TestCase,
152+
{
153+
description: 'deferred resolve + child after await',
154+
makePromise: () => new BPromise(res => setTimeout(res, 0)),
155+
thenFn: async (p, cb) => {
156+
await p;
157+
cb();
158+
}
159+
} as TestCase
160+
];
161+
162+
// tslint:disable-next-line:no-any
163+
testCases.forEach((testCase: TestCase<any>) => {
164+
it(`enables context propagation in the same way as native promises for test case: ${
165+
testCase.description}`,
166+
async () => {
167+
const actual = (await getTracesForPromiseImplementation(
168+
testCase.makePromise, testCase.thenFn))
169+
.map(trace => trace.spans.length)
170+
.join(', ');
171+
// In each case, the second trace should have the child span.
172+
// The format here is "[numSpansInFirstTrace],
173+
// [numSpansInSecondTrace]".
174+
assert.strictEqual(actual, '1, 2');
175+
});
176+
});
177+
});

test/plugins/test-trace-knex.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,23 +218,18 @@ describeInterop<typeof knexTypes>('knex', (fixture) => {
218218
return span.name === 'mysql-query';
219219
});
220220
let expectedCmds;
221-
if (parsedVersion.minor === 10 || parsedVersion.minor >= 14) {
221+
if (parsedVersion.minor === 10 || parsedVersion.minor >= 12) {
222222
expectedCmds = [
223223
/^BEGIN/, 'insert into `t` (`k`, `v`) values (?, ?)',
224224
'select * from `t`', /^ROLLBACK/, 'select * from `t`'
225225
];
226-
} else if (parsedVersion.minor === 11) {
226+
} else /*if (parsedVersion.minor === 11)*/ {
227227
expectedCmds = [
228228
'SELECT 1', 'BEGIN;',
229229
'insert into `t` (`k`, `v`) values (?, ?)',
230230
'select * from `t`', 'ROLLBACK;', 'SELECT 1',
231231
'select * from `t`'
232232
];
233-
} else {
234-
expectedCmds = [
235-
'insert into `t` (`k`, `v`) values (?, ?)',
236-
'select * from `t`', 'ROLLBACK;', 'select * from `t`'
237-
];
238233
}
239234
assert.strictEqual(expectedCmds.length, spans.length);
240235
for (let i = 0; i < spans.length; i++) {

test/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ export function wait(ms: number) {
5252
return new Promise(resolve => setTimeout(resolve, ms));
5353
}
5454

55+
// Get the given span's duration in MS.
56+
export function getDuration(span: TraceSpan) {
57+
return Date.parse(span.endTime) - Date.parse(span.startTime);
58+
}
59+
5560
// Assert that the given span's duration is within the given range.
5661
export function assertSpanDuration(span: TraceSpan, bounds: [number, number?]) {
57-
const spanDuration = Date.parse(span.endTime) - Date.parse(span.startTime);
62+
const spanDuration = getDuration(span);
5863
const lowerBound = bounds[0];
5964
const upperBound = bounds[1] !== undefined ? bounds[1] : bounds[0];
6065
assert.ok(

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"include": [
1010
"src/*.ts",
1111
"src/cls/*.ts",
12+
"src/plugins/plugin-bluebird.ts",
1213
"src/plugins/plugin-connect.ts",
1314
"src/plugins/plugin-express.ts",
1415
"src/plugins/plugin-grpc.ts",
@@ -19,6 +20,7 @@
1920
"src/plugins/plugin-koa.ts",
2021
"src/plugins/plugin-pg.ts",
2122
"src/plugins/plugin-restify.ts",
23+
"test/plugins/test-cls-bluebird.ts",
2224
"test/plugins/test-trace-google-gax.ts",
2325
"test/plugins/test-trace-http.ts",
2426
"test/plugins/test-trace-http2.ts",

0 commit comments

Comments
 (0)