Skip to content

Commit f489c94

Browse files
authored
feat: (observability) trace Database.batchCreateSessions + SessionPool.createSessions (#2145)
This change adds tracing for Database.batchCreateSessions as well as SessionPool.createSessions which was raised as a big need. This change is a premise to finishing up tracing Transaction. While here, also folded in the async/await fix to avoid day+ long code review lag and then 3+ hours just to run tests per PR: OpenTelemetry cannot work correctly for async/await if there isn't a set AsyncHooksManager, but we should not burden our customers with this type of specialist knowledge, their code should just work and this change performs such a check. Later on we shall file a feature request with the OpenTelemetry-JS API group to give us a hook to detect if we've got a live asyncHooksManager instead of this mandatory comparison to ROOT_CONTEXT each time. Fixes #2146 Updates #2079 Spun out of PR #2122 Supersedes PR #2147
1 parent a464bdb commit f489c94

File tree

11 files changed

+982
-186
lines changed

11 files changed

+982
-186
lines changed

observability-test/database.ts

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,115 @@ describe('Database', () => {
375375
});
376376
});
377377

378+
describe('batchCreateSessions', () => {
379+
it('without error', done => {
380+
const ARGS = [null, [{}]];
381+
database.request = (config, callback) => {
382+
callback(...ARGS);
383+
};
384+
385+
database.batchCreateSessions(10, (err, sessions) => {
386+
assert.ifError(err);
387+
assert.ok(sessions);
388+
389+
traceExporter.forceFlush();
390+
const spans = traceExporter.getFinishedSpans();
391+
392+
const actualSpanNames: string[] = [];
393+
const actualEventNames: string[] = [];
394+
spans.forEach(span => {
395+
actualSpanNames.push(span.name);
396+
span.events.forEach(event => {
397+
actualEventNames.push(event.name);
398+
});
399+
});
400+
401+
const expectedSpanNames = ['CloudSpanner.Database.batchCreateSessions'];
402+
assert.deepStrictEqual(
403+
actualSpanNames,
404+
expectedSpanNames,
405+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
406+
);
407+
408+
// Ensure that the span didn't encounter an error.
409+
const firstSpan = spans[0];
410+
assert.strictEqual(
411+
SpanStatusCode.UNSET,
412+
firstSpan.status.code,
413+
'Unexpected span status code'
414+
);
415+
assert.strictEqual(
416+
undefined,
417+
firstSpan.status.message,
418+
'Mismatched span status message'
419+
);
420+
421+
// We don't expect events.
422+
const expectedEventNames = [];
423+
assert.deepStrictEqual(
424+
actualEventNames,
425+
expectedEventNames,
426+
`Unexpected events:\n\tGot: ${actualEventNames}\n\tWant: ${expectedEventNames}`
427+
);
428+
429+
done();
430+
});
431+
});
432+
433+
it('with error', done => {
434+
const ARGS = [new Error('batchCreateSessions.error'), null];
435+
database.request = (config, callback) => {
436+
callback(...ARGS);
437+
};
438+
439+
database.batchCreateSessions(10, (err, sessions) => {
440+
assert.ok(err);
441+
assert.ok(!sessions);
442+
traceExporter.forceFlush();
443+
const spans = traceExporter.getFinishedSpans();
444+
445+
const actualSpanNames: string[] = [];
446+
const actualEventNames: string[] = [];
447+
spans.forEach(span => {
448+
actualSpanNames.push(span.name);
449+
span.events.forEach(event => {
450+
actualEventNames.push(event.name);
451+
});
452+
});
453+
454+
const expectedSpanNames = ['CloudSpanner.Database.batchCreateSessions'];
455+
assert.deepStrictEqual(
456+
actualSpanNames,
457+
expectedSpanNames,
458+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
459+
);
460+
461+
// Ensure that the span actually produced an error that was recorded.
462+
const firstSpan = spans[0];
463+
assert.strictEqual(
464+
SpanStatusCode.ERROR,
465+
firstSpan.status.code,
466+
'Expected an ERROR span status'
467+
);
468+
assert.strictEqual(
469+
'batchCreateSessions.error',
470+
firstSpan.status.message,
471+
'Mismatched span status message'
472+
);
473+
474+
// We don't expect events.
475+
const expectedEventNames = [];
476+
assert.deepStrictEqual(
477+
actualEventNames,
478+
expectedEventNames,
479+
`Unexpected events:\n\tGot: ${actualEventNames}\n\tWant: ${expectedEventNames}`
480+
);
481+
482+
done();
483+
});
484+
});
485+
});
486+
378487
describe('getSnapshot', () => {
379488
let fakePool: FakeSessionPool;
380489
let fakeSession: FakeSession;
@@ -409,7 +518,7 @@ describe('Database', () => {
409518

410519
getSessionStub.callsFake(callback => callback(fakeError, null));
411520

412-
database.getSnapshot((err, snapshot) => {
521+
database.getSnapshot(err => {
413522
assert.strictEqual(err, fakeError);
414523
traceExporter.forceFlush();
415524
const spans = traceExporter.getFinishedSpans();
@@ -1027,7 +1136,6 @@ describe('Database', () => {
10271136
});
10281137

10291138
it('with error on null mutation should catch thrown error', done => {
1030-
const fakeError = new Error('err');
10311139
try {
10321140
database.writeAtLeastOnce(null, (err, res) => {});
10331141
} catch (err) {

observability-test/session-pool.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*!
2+
* Copyright 2024 Google LLC. All Rights Reserved.
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+
import {before, beforeEach, afterEach, describe, it} from 'mocha';
19+
import * as extend from 'extend';
20+
import PQueue from 'p-queue';
21+
import * as proxyquire from 'proxyquire';
22+
import * as sinon from 'sinon';
23+
import stackTrace = require('stack-trace');
24+
const {
25+
AlwaysOnSampler,
26+
NodeTracerProvider,
27+
InMemorySpanExporter,
28+
} = require('@opentelemetry/sdk-trace-node');
29+
// eslint-disable-next-line n/no-extraneous-require
30+
const {SimpleSpanProcessor} = require('@opentelemetry/sdk-trace-base');
31+
// eslint-disable-next-line n/no-extraneous-require
32+
const {SpanStatusCode} = require('@opentelemetry/api');
33+
34+
import {Database} from '../src/database';
35+
import {Session} from '../src/session';
36+
import * as sp from '../src/session-pool';
37+
38+
let pQueueOverride: typeof PQueue | null = null;
39+
40+
function FakePQueue(options) {
41+
return new (pQueueOverride || PQueue)(options);
42+
}
43+
44+
FakePQueue.default = FakePQueue;
45+
46+
class FakeTransaction {
47+
options;
48+
constructor(options?) {
49+
this.options = options;
50+
}
51+
async begin(): Promise<void> {}
52+
}
53+
54+
const fakeStackTrace = extend({}, stackTrace);
55+
56+
describe('SessionPool', () => {
57+
let sessionPool: sp.SessionPool;
58+
// tslint:disable-next-line variable-name
59+
let SessionPool: typeof sp.SessionPool;
60+
61+
function noop() {}
62+
const DATABASE = {
63+
batchCreateSessions: noop,
64+
databaseRole: 'parent_role',
65+
} as unknown as Database;
66+
67+
const sandbox = sinon.createSandbox();
68+
const shouldNotBeCalled = sandbox.stub().throws('Should not be called.');
69+
70+
const createSession = (name = 'id', props?): Session => {
71+
props = props || {};
72+
73+
return Object.assign(new Session(DATABASE, name), props, {
74+
create: sandbox.stub().resolves(),
75+
delete: sandbox.stub().resolves(),
76+
keepAlive: sandbox.stub().resolves(),
77+
transaction: sandbox.stub().returns(new FakeTransaction()),
78+
});
79+
};
80+
81+
before(() => {
82+
SessionPool = proxyquire('../src/session-pool.js', {
83+
'p-queue': FakePQueue,
84+
'stack-trace': fakeStackTrace,
85+
}).SessionPool;
86+
});
87+
88+
afterEach(() => {
89+
pQueueOverride = null;
90+
sandbox.restore();
91+
});
92+
93+
const traceExporter = new InMemorySpanExporter();
94+
const sampler = new AlwaysOnSampler();
95+
const provider = new NodeTracerProvider({
96+
sampler: sampler,
97+
exporter: traceExporter,
98+
});
99+
provider.addSpanProcessor(new SimpleSpanProcessor(traceExporter));
100+
101+
beforeEach(() => {
102+
DATABASE.session = createSession;
103+
DATABASE._observabilityOptions = {
104+
tracerProvider: provider,
105+
};
106+
sessionPool = new SessionPool(DATABASE);
107+
sessionPool._observabilityOptions = DATABASE._observabilityOptions;
108+
traceExporter.reset();
109+
});
110+
111+
describe('_createSessions', () => {
112+
const OPTIONS = 3;
113+
it('on exception from Database.batchCreateSessions', async () => {
114+
const ourException = new Error('this fails intentionally');
115+
const stub = sandbox
116+
.stub(DATABASE, 'batchCreateSessions')
117+
.throws(ourException);
118+
const releaseStub = sandbox.stub(sessionPool, 'release');
119+
120+
assert.rejects(async () => {
121+
await sessionPool._createSessions(OPTIONS);
122+
}, ourException);
123+
124+
traceExporter.forceFlush();
125+
const spans = traceExporter.getFinishedSpans();
126+
127+
const actualSpanNames: string[] = [];
128+
const actualEventNames: string[] = [];
129+
spans.forEach(span => {
130+
actualSpanNames.push(span.name);
131+
span.events.forEach(event => {
132+
actualEventNames.push(event.name);
133+
});
134+
});
135+
136+
const expectedSpanNames = ['CloudSpanner.SessionPool.createSessions'];
137+
assert.deepStrictEqual(
138+
actualSpanNames,
139+
expectedSpanNames,
140+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
141+
);
142+
143+
const expectedEventNames = [
144+
'Requesting 3 sessions',
145+
'Creating 3 sessions',
146+
'Requested for 3 sessions returned 0',
147+
'exception',
148+
];
149+
assert.deepStrictEqual(
150+
actualEventNames,
151+
expectedEventNames,
152+
`Unexpected events:\n\tGot: ${actualEventNames}\n\tWant: ${expectedEventNames}`
153+
);
154+
155+
const firstSpan = spans[0];
156+
assert.strictEqual(
157+
SpanStatusCode.ERROR,
158+
firstSpan.status.code,
159+
'Unexpected an span status code'
160+
);
161+
assert.strictEqual(
162+
ourException.message,
163+
firstSpan.status.message,
164+
'Unexpected span status message'
165+
);
166+
});
167+
168+
it('without error', async () => {
169+
const RESPONSE = [[{}, {}, {}]];
170+
171+
const stub = sandbox
172+
.stub(DATABASE, 'batchCreateSessions')
173+
.resolves(RESPONSE);
174+
const releaseStub = sandbox.stub(sessionPool, 'release');
175+
176+
await sessionPool._createSessions(OPTIONS);
177+
assert.strictEqual(sessionPool.size, 3);
178+
179+
traceExporter.forceFlush();
180+
const spans = traceExporter.getFinishedSpans();
181+
182+
const actualSpanNames: string[] = [];
183+
const actualEventNames: string[] = [];
184+
spans.forEach(span => {
185+
actualSpanNames.push(span.name);
186+
span.events.forEach(event => {
187+
actualEventNames.push(event.name);
188+
});
189+
});
190+
191+
const expectedSpanNames = ['CloudSpanner.SessionPool.createSessions'];
192+
assert.deepStrictEqual(
193+
actualSpanNames,
194+
expectedSpanNames,
195+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
196+
);
197+
198+
const expectedEventNames = [
199+
'Requesting 3 sessions',
200+
'Creating 3 sessions',
201+
'Requested for 3 sessions returned 3',
202+
];
203+
assert.deepStrictEqual(
204+
actualEventNames,
205+
expectedEventNames,
206+
`Unexpected events:\n\tGot: ${actualEventNames}\n\tWant: ${expectedEventNames}`
207+
);
208+
209+
const firstSpan = spans[0];
210+
assert.strictEqual(
211+
SpanStatusCode.UNSET,
212+
firstSpan.status.code,
213+
'Unexpected an span status code'
214+
);
215+
assert.strictEqual(
216+
undefined,
217+
firstSpan.status.message,
218+
'Unexpected span status message'
219+
);
220+
});
221+
});
222+
});

0 commit comments

Comments
 (0)