Skip to content

Commit d8ab278

Browse files
authored
observability: add SessionPool span annotations (#2101)
This change adds span annotations on the active span which could either be the noopSpan or the top-level active one, for whenever we progressively add more spans. Built from #2087 Updates #2079
1 parent 84b99ef commit d8ab278

File tree

7 files changed

+301
-12
lines changed

7 files changed

+301
-12
lines changed

observability-test/observability.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,8 @@ describe('startTrace', () => {
7777
startTrace('mySpan', {}, span => {
7878
span.end();
7979

80-
const spans = globalExporter.getFinishedSpans();
81-
assert.strictEqual(
82-
spans.length,
83-
1,
84-
'Exactly 1 span must have been exported'
85-
);
86-
const span0 = spans[0];
87-
8880
assert.equal(
89-
span0.name,
81+
span.name,
9082
SPAN_NAMESPACE_PREFIX + '.mySpan',
9183
'name mismatch'
9284
);
@@ -317,7 +309,7 @@ describe('getActiveOrNoopSpan', () => {
317309
assert.strictEqual(
318310
span.name,
319311
activeSpan.name,
320-
'names must match between activeSpan or current one'
312+
`names must match between activeSpan or current one\n\tGot: ${span.name}\n\tWant: ${activeSpan.name}`
321313
);
322314
assert.strictEqual(
323315
span.startTime,

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
"@google-cloud/promisify": "^4.0.0",
6161
"@grpc/proto-loader": "^0.7.0",
6262
"@opentelemetry/api": "^1.9.0",
63-
"@opentelemetry/context-async-hooks": "^1.25.1",
6463
"@opentelemetry/semantic-conventions": "^1.25.1",
6564
"@types/big.js": "^6.0.0",
6665
"@types/stack-trace": "0.0.33",
@@ -86,6 +85,9 @@
8685
"through2": "^4.0.0"
8786
},
8887
"devDependencies": {
88+
"@opentelemetry/context-async-hooks": "^1.25.1",
89+
"@opentelemetry/sdk-trace-base": "^1.26.0",
90+
"@opentelemetry/sdk-trace-node": "^1.26.0",
8991
"@types/concat-stream": "^2.0.0",
9092
"@types/extend": "^3.0.0",
9193
"@types/is": "0.0.25",
@@ -116,7 +118,6 @@
116118
"mv": "^2.1.1",
117119
"ncp": "^2.0.0",
118120
"nise": "6.0.0",
119-
"@opentelemetry/sdk-trace-node": "^1.25.1",
120121
"p-limit": "^3.0.1",
121122
"path-to-regexp": "6.2.2",
122123
"proxyquire": "^2.0.1",

src/database.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import Policy = google.iam.v1.Policy;
102102
import FieldMask = google.protobuf.FieldMask;
103103
import IDatabase = google.spanner.admin.database.v1.IDatabase;
104104
import snakeCase = require('lodash.snakecase');
105+
import {getActiveOrNoopSpan} from './instrument';
105106

106107
export type GetDatabaseRolesCallback = RequestCallback<
107108
IDatabaseRole,
@@ -824,10 +825,17 @@ class Database extends common.GrpcServiceObject {
824825
const transaction = this.batchTransaction({session: session!}, options);
825826
this._releaseOnEnd(session!, transaction);
826827
transaction.begin((err, resp) => {
828+
const span = getActiveOrNoopSpan();
827829
if (err) {
830+
if (isSessionNotFoundError(err)) {
831+
span.addEvent('No session available', {
832+
'session.id': session?.id,
833+
});
834+
}
828835
callback!(err, null, resp!);
829836
return;
830837
}
838+
span.addEvent('Using Session', {'session.id': session?.id});
831839
callback!(null, transaction, resp!);
832840
});
833841
});
@@ -2031,12 +2039,17 @@ class Database extends common.GrpcServiceObject {
20312039
const snapshot = session!.snapshot(options, this.queryOptions_);
20322040

20332041
snapshot.begin(err => {
2042+
const span = getActiveOrNoopSpan();
20342043
if (err) {
20352044
if (isSessionNotFoundError(err)) {
2045+
span.addEvent('No session available', {
2046+
'session.id': session?.id,
2047+
});
20362048
session!.lastError = err;
20372049
this.pool_.release(session!);
20382050
this.getSnapshot(options, callback!);
20392051
} else {
2052+
span.addEvent('Using Session', {'session.id': session?.id});
20402053
this.pool_.release(session!);
20412054
callback!(err);
20422055
}
@@ -2121,7 +2134,14 @@ class Database extends common.GrpcServiceObject {
21212134
transaction!.excludeTxnFromChangeStreams();
21222135
}
21232136
if (!err) {
2137+
const span = getActiveOrNoopSpan();
2138+
span.addEvent('Using Session', {'session.id': session?.id});
21242139
this._releaseOnEnd(session!, transaction!);
2140+
} else if (isSessionNotFoundError(err as grpc.ServiceError)) {
2141+
const span = getActiveOrNoopSpan();
2142+
span.addEvent('No session available', {
2143+
'session.id': session?.id,
2144+
});
21252145
}
21262146
cb!(err as grpc.ServiceError | null, transaction);
21272147
});
@@ -2348,6 +2368,8 @@ class Database extends common.GrpcServiceObject {
23482368
callback!(err as ServiceError, null);
23492369
return;
23502370
}
2371+
const span = getActiveOrNoopSpan();
2372+
span.addEvent('Using Session', {'session.id': session?.id});
23512373
config.reqOpts.session = session!.formattedName_;
23522374
this.request<Session>(config, (err, ...args) => {
23532375
pool.release(session!);
@@ -2389,10 +2411,17 @@ class Database extends common.GrpcServiceObject {
23892411
}
23902412
waitForSessionStream.on('reading', () => {
23912413
pool.getSession((err, session_) => {
2414+
const span = getActiveOrNoopSpan();
23922415
if (err) {
2416+
if (isSessionNotFoundError(err as grpc.ServiceError)) {
2417+
span.addEvent('No session available', {
2418+
'session.id': session?.id,
2419+
});
2420+
}
23932421
destroyStream(err as ServiceError);
23942422
return;
23952423
}
2424+
span.addEvent('Using Session', {'session.id': session_?.id});
23962425
session = session_!;
23972426
config.reqOpts.session = session!.formattedName_;
23982427
requestStream = self.requestStream(config);
@@ -2924,13 +2953,17 @@ class Database extends common.GrpcServiceObject {
29242953
options?: TimestampBounds
29252954
): PartialResultStream {
29262955
const proxyStream: Transform = through.obj();
2956+
const span = getActiveOrNoopSpan();
29272957

29282958
this.pool_.getSession((err, session) => {
29292959
if (err) {
29302960
proxyStream.destroy(err);
29312961
return;
29322962
}
29332963

2964+
const span = getActiveOrNoopSpan();
2965+
span.addEvent('Using Session', {'session.id': session?.id});
2966+
29342967
const snapshot = session!.snapshot(options, this.queryOptions_);
29352968

29362969
this._releaseOnEnd(session!, snapshot);
@@ -2951,6 +2984,9 @@ class Database extends common.GrpcServiceObject {
29512984
if (session) {
29522985
session.lastError = err as grpc.ServiceError;
29532986
}
2987+
span.addEvent('No session available', {
2988+
'session.id': session?.id,
2989+
});
29542990
// Remove the current data stream from the end user stream.
29552991
dataStream.unpipe(proxyStream);
29562992
dataStream.removeListener('end', endListener);
@@ -3081,7 +3117,11 @@ class Database extends common.GrpcServiceObject {
30813117
: {};
30823118

30833119
this.pool_.getSession((err, session?, transaction?) => {
3120+
const span = getActiveOrNoopSpan();
30843121
if (err && isSessionNotFoundError(err as grpc.ServiceError)) {
3122+
span.addEvent('No session available', {
3123+
'session.id': session?.id,
3124+
});
30853125
this.runTransaction(options, runFn!);
30863126
return;
30873127
}
@@ -3105,10 +3145,17 @@ class Database extends common.GrpcServiceObject {
31053145
);
31063146

31073147
runner.run().then(release, err => {
3148+
const span = getActiveOrNoopSpan();
31083149
if (isSessionNotFoundError(err)) {
3150+
span.addEvent('No session available', {
3151+
'session.id': session?.id,
3152+
});
31093153
release();
31103154
this.runTransaction(options, runFn!);
31113155
} else {
3156+
if (!err) {
3157+
span.addEvent('Using Session', {'session.id': session!.id});
3158+
}
31123159
setImmediate(runFn!, err);
31133160
release();
31143161
}
@@ -3195,7 +3242,9 @@ class Database extends common.GrpcServiceObject {
31953242
? (optionsOrRunFn as RunTransactionOptions)
31963243
: {};
31973244

3245+
let sessionId = '';
31983246
const getSession = this.pool_.getSession.bind(this.pool_);
3247+
const span = getActiveOrNoopSpan();
31993248
// Loop to retry 'Session not found' errors.
32003249
// (and yes, we like while (true) more than for (;;) here)
32013250
// eslint-disable-next-line no-constant-condition
@@ -3212,6 +3261,8 @@ class Database extends common.GrpcServiceObject {
32123261
if (options.excludeTxnFromChangeStreams) {
32133262
transaction.excludeTxnFromChangeStreams();
32143263
}
3264+
sessionId = session?.id;
3265+
span.addEvent('Using Session', {'session.id': sessionId});
32153266
const runner = new AsyncTransactionRunner<T>(
32163267
session,
32173268
transaction,
@@ -3226,6 +3277,9 @@ class Database extends common.GrpcServiceObject {
32263277
}
32273278
} catch (e) {
32283279
if (!isSessionNotFoundError(e as ServiceError)) {
3280+
span.addEvent('No session available', {
3281+
'session.id': sessionId,
3282+
});
32293283
throw e;
32303284
}
32313285
}
@@ -3301,6 +3355,10 @@ class Database extends common.GrpcServiceObject {
33013355
proxyStream.destroy(err);
33023356
return;
33033357
}
3358+
3359+
const span = getActiveOrNoopSpan();
3360+
span.addEvent('Using Session', {'session.id': session?.id});
3361+
33043362
const gaxOpts = extend(true, {}, options?.gaxOptions);
33053363
const reqOpts = Object.assign(
33063364
{} as spannerClient.spanner.v1.BatchWriteRequest,
@@ -3332,6 +3390,7 @@ class Database extends common.GrpcServiceObject {
33323390
if (session) {
33333391
session.lastError = err as grpc.ServiceError;
33343392
}
3393+
span.addEvent('No session available', {'session.id': session?.id});
33353394
// Remove the current data stream from the end user stream.
33363395
dataStream.unpipe(proxyStream);
33373396
dataStream.end();
@@ -3424,14 +3483,19 @@ class Database extends common.GrpcServiceObject {
34243483
? (optionsOrCallback as CallOptions)
34253484
: {};
34263485
this.pool_.getSession((err, session?, transaction?) => {
3486+
const span = getActiveOrNoopSpan();
34273487
if (err && isSessionNotFoundError(err as grpc.ServiceError)) {
3488+
span.addEvent('No session available', {
3489+
'session.id': session?.id,
3490+
});
34283491
this.writeAtLeastOnce(mutations, options, cb!);
34293492
return;
34303493
}
34313494
if (err) {
34323495
cb!(err as grpc.ServiceError);
34333496
return;
34343497
}
3498+
span.addEvent('Using Session', {'session.id': session?.id});
34353499
this._releaseOnEnd(session!, transaction!);
34363500
transaction?.setQueuedMutations(mutations.proto());
34373501
return transaction?.commit(options, cb!);

src/session-pool.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {Transaction} from './transaction';
2424
import {NormalCallback} from './common';
2525
import {GoogleError, grpc, ServiceError} from 'google-gax';
2626
import trace = require('stack-trace');
27+
import {getActiveOrNoopSpan} from './instrument';
2728

2829
/**
2930
* @callback SessionPoolCloseCallback
@@ -630,7 +631,9 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
630631
* @returns {Promise<Session>}
631632
*/
632633
async _acquire(): Promise<Session> {
634+
const span = getActiveOrNoopSpan();
633635
if (!this.isOpen) {
636+
span.addEvent('SessionPool is closed');
634637
throw new GoogleError(errors.Closed);
635638
}
636639

@@ -642,18 +645,30 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
642645
// wrapping this logic in a function to call recursively if the session
643646
// we end up with is already dead
644647
const getSession = async (): Promise<Session> => {
648+
span.addEvent('Acquiring session');
645649
const elapsed = Date.now() - startTime;
646650

647651
if (elapsed >= timeout!) {
652+
span.addEvent('Could not acquire session due to an exceeded timeout');
648653
throw new GoogleError(errors.Timeout);
649654
}
650655

651656
const session = await this._getSession(startTime);
652657

653658
if (this._isValidSession(session)) {
659+
span.addEvent('Acquired session', {
660+
'time.elapsed': Date.now() - startTime,
661+
'session.id': session.id.toString(),
662+
});
654663
return session;
655664
}
656665

666+
span.addEvent(
667+
'Could not acquire session because it was invalid. Retrying',
668+
{
669+
'session.id': session.id.toString(),
670+
}
671+
);
657672
this._inventory.borrowed.delete(session);
658673
return getSession();
659674
};
@@ -723,6 +738,9 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
723738
* @emits SessionPool#createError
724739
*/
725740
async _createSessions(amount: number): Promise<void> {
741+
const span = getActiveOrNoopSpan();
742+
span.addEvent(`Requesting ${amount} sessions`);
743+
726744
const labels = this.options.labels!;
727745
const databaseRole = this.options.databaseRole!;
728746

@@ -731,11 +749,16 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
731749
}
732750
this._pending += amount;
733751

752+
let nReturned = 0;
753+
const nRequested: number = amount;
754+
734755
// while we can request as many sessions be created as we want, the backend
735756
// will return at most 100 at a time, hence the need for a while loop.
736757
while (amount > 0) {
737758
let sessions: Session[] | null = null;
738759

760+
span.addEvent(`Creating ${amount} sessions`);
761+
739762
try {
740763
[sessions] = await this.database.batchCreateSessions({
741764
count: amount,
@@ -744,9 +767,13 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
744767
});
745768

746769
amount -= sessions.length;
770+
nReturned += sessions.length;
747771
} catch (e) {
748772
this._pending -= amount;
749773
this.emit('createError', e);
774+
span.addEvent(
775+
`Requested for ${nRequested} sessions returned ${nReturned}`
776+
);
750777
throw e;
751778
}
752779

@@ -758,6 +785,8 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
758785
});
759786
});
760787
}
788+
789+
span.addEvent(`Requested for ${nRequested} sessions returned ${nReturned}`);
761790
}
762791

763792
/**
@@ -865,17 +894,21 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
865894
* @returns {Promise<Session>}
866895
*/
867896
async _getSession(startTime: number): Promise<Session> {
897+
const span = getActiveOrNoopSpan();
868898
if (this._hasSessionUsableFor()) {
899+
span.addEvent('Cache hit: has usable session');
869900
return this._borrowNextAvailableSession();
870901
}
871902
if (this.isFull && this.options.fail!) {
903+
span.addEvent('Session pool is full and failFast=true');
872904
throw new SessionPoolExhaustedError(this._getLeaks());
873905
}
874906

875907
let removeOnceCloseListener: Function;
876908
let removeListener: Function;
877909

878910
// Wait for a session to become available.
911+
span.addEvent('Waiting for a session to become available');
879912
const availableEvent = 'session-available';
880913
const promises = [
881914
new Promise((_, reject) => {
@@ -990,6 +1023,10 @@ export class SessionPool extends EventEmitter implements SessionPoolInterface {
9901023
* @returns {Promise}
9911024
*/
9921025
async _ping(session: Session): Promise<void> {
1026+
// NOTE: Please do not trace Ping as it gets quite spammy
1027+
// with many root spans polluting the main span.
1028+
// Please see https://github.com/googleapis/google-cloud-go/issues/1691
1029+
9931030
this._borrow(session);
9941031

9951032
if (!this._isValidSession(session)) {

0 commit comments

Comments
 (0)