Skip to content

Commit c6e430f

Browse files
committed
feat(observability): trace Database methods
Adds trace spans for Database methods, as well as tests for methods: * getSession * getSnapshot * run * runStream * runTransaction tracing of other methods shall come in follow-up PRs. Updates googleapis#2079 Built from PR googleapis#2087 Updates googleapis#2114
1 parent d51aae9 commit c6e430f

File tree

2 files changed

+632
-209
lines changed

2 files changed

+632
-209
lines changed

observability-test/database.ts

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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 {grpc} from 'google-gax';
19+
import {google} from '../protos/protos';
20+
import {Database, Instance, SessionPool, Snapshot, Spanner} from '../src';
21+
import protobuf = google.spanner.v1;
22+
import * as mock from '../test/mockserver/mockspanner';
23+
import * as mockInstanceAdmin from '../test/mockserver/mockinstanceadmin';
24+
import * as mockDatabaseAdmin from '../test/mockserver/mockdatabaseadmin';
25+
const {
26+
AlwaysOnSampler,
27+
NodeTracerProvider,
28+
InMemorySpanExporter,
29+
} = require('@opentelemetry/sdk-trace-node');
30+
// eslint-disable-next-line n/no-extraneous-require
31+
const {SimpleSpanProcessor} = require('@opentelemetry/sdk-trace-base');
32+
33+
/** A simple result set for SELECT 1. */
34+
function createSelect1ResultSet(): protobuf.ResultSet {
35+
const fields = [
36+
protobuf.StructType.Field.create({
37+
name: 'NUM',
38+
type: protobuf.Type.create({code: protobuf.TypeCode.INT64}),
39+
}),
40+
];
41+
const metadata = new protobuf.ResultSetMetadata({
42+
rowType: new protobuf.StructType({
43+
fields,
44+
}),
45+
});
46+
return protobuf.ResultSet.create({
47+
metadata,
48+
rows: [{values: [{stringValue: '1'}]}],
49+
});
50+
}
51+
52+
interface setupResults {
53+
server: grpc.Server;
54+
spanner: Spanner;
55+
spannerMock: mock.MockSpanner;
56+
}
57+
58+
async function setup(): Promise<setupResults> {
59+
const server = new grpc.Server();
60+
61+
const spannerMock = mock.createMockSpanner(server);
62+
mockInstanceAdmin.createMockInstanceAdmin(server);
63+
mockDatabaseAdmin.createMockDatabaseAdmin(server);
64+
65+
const port: number = await new Promise((resolve, reject) => {
66+
server.bindAsync(
67+
'0.0.0.0:0',
68+
grpc.ServerCredentials.createInsecure(),
69+
(err, assignedPort) => {
70+
if (err) {
71+
reject(err);
72+
} else {
73+
resolve(assignedPort);
74+
}
75+
}
76+
);
77+
});
78+
79+
const selectSql = 'SELECT 1';
80+
const updateSql = 'UPDATE FOO SET BAR=1 WHERE BAZ=2';
81+
spannerMock.putStatementResult(
82+
selectSql,
83+
mock.StatementResult.resultSet(createSelect1ResultSet())
84+
);
85+
spannerMock.putStatementResult(
86+
updateSql,
87+
mock.StatementResult.updateCount(1)
88+
);
89+
90+
const spanner = new Spanner({
91+
projectId: 'observability-project-id',
92+
servicePath: 'localhost',
93+
port,
94+
sslCreds: grpc.credentials.createInsecure(),
95+
});
96+
97+
return Promise.resolve({
98+
spanner: spanner,
99+
server: server,
100+
spannerMock: spannerMock,
101+
});
102+
}
103+
104+
describe('Database', () => {
105+
let server: grpc.Server;
106+
let spanner: Spanner;
107+
let database: Database;
108+
let spannerMock: mock.MockSpanner;
109+
let traceExporter: typeof InMemorySpanExporter;
110+
111+
after(() => {
112+
spanner.close();
113+
server.tryShutdown(() => {});
114+
});
115+
116+
before(async () => {
117+
const setupResult = await setup();
118+
spanner = setupResult.spanner;
119+
server = setupResult.server;
120+
spannerMock = setupResult.spannerMock;
121+
122+
const selectSql = 'SELECT 1';
123+
const updateSql = 'UPDATE FOO SET BAR=1 WHERE BAZ=2';
124+
spannerMock.putStatementResult(
125+
selectSql,
126+
mock.StatementResult.resultSet(createSelect1ResultSet())
127+
);
128+
spannerMock.putStatementResult(
129+
updateSql,
130+
mock.StatementResult.updateCount(1)
131+
);
132+
133+
traceExporter = new InMemorySpanExporter();
134+
const sampler = new AlwaysOnSampler();
135+
136+
const provider = new NodeTracerProvider({
137+
sampler: sampler,
138+
exporter: traceExporter,
139+
});
140+
provider.addSpanProcessor(new SimpleSpanProcessor(traceExporter));
141+
142+
const instance = spanner.instance('instance');
143+
database = instance.database('database');
144+
database.observabilityConfig = {
145+
tracerProvider: provider,
146+
enableExtendedTracing: false,
147+
};
148+
});
149+
150+
beforeEach(() => {
151+
spannerMock.resetRequests();
152+
});
153+
154+
afterEach(() => {
155+
traceExporter.reset();
156+
});
157+
158+
it('getSessions', async () => {
159+
const [rows] = await database.getSessions();
160+
161+
traceExporter.forceFlush();
162+
const spans = traceExporter.getFinishedSpans();
163+
assert.strictEqual(spans.length, 1, 'Exactly 1 span expected');
164+
165+
const actualSpanNames: string[] = [];
166+
spans.forEach(span => {
167+
actualSpanNames.push(span.name);
168+
});
169+
170+
const expectedSpanNames = ['CloudSpanner.Database.getSessions'];
171+
assert.deepStrictEqual(
172+
actualSpanNames,
173+
expectedSpanNames,
174+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
175+
);
176+
});
177+
it('getSnapshot', done => {
178+
database.getSnapshot((err, transaction) => {
179+
assert.ifError(err);
180+
181+
transaction!.run('SELECT 1', (err, rows) => {
182+
assert.ifError(err);
183+
184+
traceExporter.forceFlush();
185+
const spans = traceExporter.getFinishedSpans();
186+
assert.strictEqual(spans.length, 1, 'Exactly 1 span expected');
187+
188+
const actualSpanNames: string[] = [];
189+
spans.forEach(span => {
190+
actualSpanNames.push(span.name);
191+
});
192+
193+
const expectedSpanNames = ['CloudSpanner.Database.getSnapshot'];
194+
assert.deepStrictEqual(
195+
actualSpanNames,
196+
expectedSpanNames,
197+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
198+
);
199+
200+
done();
201+
});
202+
});
203+
});
204+
205+
it('runStream', done => {
206+
database
207+
.runStream('SELECT 1')
208+
.on('data', row => {})
209+
.on('error', assert.ifError)
210+
.on('end', () => {
211+
traceExporter.forceFlush();
212+
const spans = traceExporter.getFinishedSpans();
213+
assert.strictEqual(spans.length, 1, 'Exactly 1 span expected');
214+
215+
const actualSpanNames: string[] = [];
216+
spans.forEach(span => {
217+
actualSpanNames.push(span.name);
218+
});
219+
220+
const expectedSpanNames = ['CloudSpanner.Database.runStream'];
221+
assert.deepStrictEqual(
222+
actualSpanNames,
223+
expectedSpanNames,
224+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
225+
);
226+
227+
done();
228+
});
229+
});
230+
231+
it('run', async () => {
232+
const [rows] = await database.run('SELECT 1');
233+
234+
traceExporter.forceFlush();
235+
const spans = traceExporter.getFinishedSpans();
236+
assert.strictEqual(spans.length, 2, 'Exactly 2 spans expected');
237+
238+
// Sort the spans by duration.
239+
spans.sort((spanA, spanB) => {
240+
spanA.duration < spanB.duration;
241+
});
242+
243+
const actualSpanNames: string[] = [];
244+
spans.forEach(span => {
245+
actualSpanNames.push(span.name);
246+
});
247+
248+
const expectedSpanNames = [
249+
'CloudSpanner.Database.runStream',
250+
'CloudSpanner.Database.run',
251+
];
252+
assert.deepStrictEqual(
253+
actualSpanNames,
254+
expectedSpanNames,
255+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
256+
);
257+
258+
// Ensure that RunStream is a child span of createQueryPartitions.
259+
const spanRunStream = spans[0];
260+
const spanRun = spans[1];
261+
assert.ok(
262+
spanRun.spanContext().traceId,
263+
'Expected that createQueryPartitions has a defined traceId'
264+
);
265+
assert.ok(
266+
spanRunStream.spanContext().traceId,
267+
'Expected that RunStream has a defined traceId'
268+
);
269+
assert.deepStrictEqual(
270+
spanRunStream.spanContext().traceId,
271+
spanRun.spanContext().traceId,
272+
'Expected that both spans share a traceId'
273+
);
274+
assert.ok(
275+
spanRun.spanContext().spanId,
276+
'Expected that createQueryPartitions has a defined spanId'
277+
);
278+
assert.ok(
279+
spanRunStream.spanContext().spanId,
280+
'Expected that RunStream has a defined spanId'
281+
);
282+
assert.deepStrictEqual(
283+
spanRunStream.parentSpanId,
284+
spanRun.spanContext().spanId,
285+
'Expected that run is the parent to runStream'
286+
);
287+
});
288+
289+
it('runTransaction', done => {
290+
database.runTransaction((err, transaction) => {
291+
assert.ifError(err);
292+
transaction!.run('SELECT 1', (err, rows) => {
293+
assert.ifError(err);
294+
295+
traceExporter.forceFlush();
296+
const spans = traceExporter.getFinishedSpans();
297+
assert.strictEqual(spans.length, 1, 'Exactly 1 span expected');
298+
299+
const actualSpanNames: string[] = [];
300+
spans.forEach(span => {
301+
actualSpanNames.push(span.name);
302+
});
303+
304+
const expectedSpanNames = ['CloudSpanner.Database.runTransaction'];
305+
assert.deepStrictEqual(
306+
actualSpanNames,
307+
expectedSpanNames,
308+
`span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}`
309+
);
310+
311+
done();
312+
});
313+
});
314+
});
315+
});

0 commit comments

Comments
 (0)