diff --git a/observability-test/database.ts b/observability-test/database.ts index 8329e81eb..945b7afdc 100644 --- a/observability-test/database.ts +++ b/observability-test/database.ts @@ -241,7 +241,7 @@ describe('Database', () => { database = new Database(INSTANCE, NAME, POOL_OPTIONS); database.parent = INSTANCE; database.databaseRole = 'parent_role'; - database.observabilityConfig = { + database.observabilityOptions_ = { tracerProvider: provider, enableExtendedTracing: false, }; diff --git a/observability-test/spanner.ts b/observability-test/spanner.ts index 933e9bf08..44fd8a4e2 100644 --- a/observability-test/spanner.ts +++ b/observability-test/spanner.ts @@ -17,7 +17,7 @@ import * as assert from 'assert'; import {grpc} from 'google-gax'; import {google} from '../protos/protos'; -import {Database, Spanner} from '../src'; +import {Database, Instance, Spanner} from '../src'; import {MutationSet} from '../src/transaction'; import protobuf = google.spanner.v1; import * as mock from '../test/mockserver/mockspanner'; @@ -35,6 +35,8 @@ const { AsyncHooksContextManager, } = require('@opentelemetry/context-async-hooks'); +const {ObservabilityOptions} = require('../src/instrument'); + /** A simple result set for SELECT 1. */ function createSelect1ResultSet(): protobuf.ResultSet { const fields = [ @@ -60,7 +62,9 @@ interface setupResults { spannerMock: mock.MockSpanner; } -async function setup(): Promise { +async function setup( + observabilityOptions?: typeof ObservabilityOptions +): Promise { const server = new grpc.Server(); const spannerMock = mock.createMockSpanner(server); @@ -97,6 +101,7 @@ async function setup(): Promise { servicePath: 'localhost', port, sslCreds: grpc.credentials.createInsecure(), + observabilityOptions: observabilityOptions, }); return Promise.resolve({ @@ -149,7 +154,7 @@ describe('EndToEnd', () => { const instance = spanner.instance('instance'); database = instance.database('database'); - database.observabilityConfig = { + database.observabilityOptions_ = { tracerProvider: provider, enableExtendedTracing: false, }; @@ -440,3 +445,141 @@ describe('EndToEnd', () => { }); }); }); + +describe('ObservabilityOptions injection and propagation', async () => { + const globalTraceExporter = new InMemorySpanExporter(); + const globalTracerProvider = new NodeTracerProvider({ + sampler: new AlwaysOnSampler(), + exporter: globalTraceExporter, + }); + globalTracerProvider.addSpanProcessor( + new SimpleSpanProcessor(globalTraceExporter) + ); + globalTracerProvider.register(); + + const injectedTraceExporter = new InMemorySpanExporter(); + const injectedTracerProvider = new NodeTracerProvider({ + sampler: new AlwaysOnSampler(), + exporter: injectedTraceExporter, + }); + injectedTracerProvider.addSpanProcessor( + new SimpleSpanProcessor(injectedTraceExporter) + ); + + const observabilityOptions: typeof ObservabilityOptions = { + tracerProvider: injectedTracerProvider, + enableExtendedTracing: true, + }; + + const setupResult = await setup(observabilityOptions); + const spanner = setupResult.spanner; + const server = setupResult.server; + const spannerMock = setupResult.spannerMock; + + after(() => { + globalTraceExporter.reset(); + injectedTraceExporter.reset(); + spannerMock.resetRequests(); + spanner.close(); + server.tryShutdown(() => {}); + }); + + it('Passed into Spanner, Instance and Database', done => { + // Ensure that the same observability configuration is set on the Spanner client. + assert.deepStrictEqual(spanner.observabilityOptions_, observabilityOptions); + + // Acquire a handle to the Instance through spanner.instance. + const instanceByHandle = spanner.instance('instance'); + assert.deepStrictEqual( + instanceByHandle.observabilityOptions_, + observabilityOptions + ); + + // Create the Instance by means of a constructor directly. + const instanceByConstructor = new Instance(spanner, 'myInstance'); + assert.deepStrictEqual( + instanceByConstructor.observabilityOptions_, + observabilityOptions + ); + + // Acquire a handle to the Database through instance.database. + const databaseByHandle = instanceByHandle.database('database'); + assert.deepStrictEqual( + databaseByHandle.observabilityOptions_, + observabilityOptions + ); + + // Create the Database by means of a constructor directly. + const databaseByConstructor = new Database( + instanceByConstructor, + 'myDatabase' + ); + assert.deepStrictEqual( + databaseByConstructor.observabilityOptions_, + observabilityOptions + ); + + done(); + }); + + it('Propagates spans to the injected not global TracerProvider', done => { + const instance = spanner.instance('instance'); + const database = instance.database('database'); + + database.run('SELECT 1', (err, rows) => { + assert.ifError(err); + + injectedTraceExporter.forceFlush(); + globalTraceExporter.forceFlush(); + const spansFromInjected = injectedTraceExporter.getFinishedSpans(); + const spansFromGlobal = globalTraceExporter.getFinishedSpans(); + + assert.strictEqual( + spansFromGlobal.length, + 0, + 'Expecting no spans from the global exporter' + ); + assert.strictEqual( + spansFromInjected.length > 0, + true, + 'Expecting spans from the injected exporter' + ); + + spansFromInjected.sort((spanA, spanB) => { + spanA.startTime < spanB.startTime; + }); + const actualSpanNames: string[] = []; + const actualEventNames: string[] = []; + spansFromInjected.forEach(span => { + actualSpanNames.push(span.name); + span.events.forEach(event => { + actualEventNames.push(event.name); + }); + }); + + const expectedSpanNames = [ + 'CloudSpanner.Database.runStream', + 'CloudSpanner.Database.run', + ]; + assert.deepStrictEqual( + actualSpanNames, + expectedSpanNames, + `span names mismatch:\n\tGot: ${actualSpanNames}\n\tWant: ${expectedSpanNames}` + ); + + const expectedEventNames = [ + 'Acquiring session', + 'Waiting for a session to become available', + 'Acquired session', + 'Using Session', + ]; + assert.deepStrictEqual( + actualEventNames, + expectedEventNames, + `Unexpected events:\n\tGot: ${actualEventNames}\n\tWant: ${expectedEventNames}` + ); + + done(); + }); + }); +}); diff --git a/src/database.ts b/src/database.ts index 0800bb737..28eb7a451 100644 --- a/src/database.ts +++ b/src/database.ts @@ -693,7 +693,7 @@ class Database extends common.GrpcServiceObject { const sessions = (resp!.session || []).map(metadata => { const session = this.session(metadata.name!); - session.observabilityConfig = this.observabilityConfig; + session.observabilityOptions_ = this.observabilityOptions_; session.metadata = metadata; return session; }); @@ -738,6 +738,7 @@ class Database extends common.GrpcServiceObject { const id = identifier.transaction; const transaction = new BatchTransaction(session, options); transaction.id = id; + transaction.observabilityOptions_ = this.observabilityOptions_; transaction.readTimestamp = identifier.timestamp as PreciseDate; return transaction; } @@ -827,7 +828,7 @@ class Database extends common.GrpcServiceObject { ? (optionsOrCallback as TimestampBounds) : {}; - const q = {opts: this.observabilityConfig}; + const q = {opts: this.observabilityOptions_}; return startTrace('Database.createBatchTransaction', q, span => { this.pool_.getSession((err, session) => { if (err) { @@ -1874,7 +1875,7 @@ class Database extends common.GrpcServiceObject { delete (gaxOpts as GetSessionsOptions).pageToken; } - const q = {opts: this.observabilityConfig}; + const q = {opts: this.observabilityOptions_}; return startTrace('Database.getSessions', q, span => { this.request< google.spanner.v1.ISession, @@ -1896,7 +1897,7 @@ class Database extends common.GrpcServiceObject { sessionInstances = sessions.map(metadata => { const session = self.session(metadata.name!); session.metadata = metadata; - session.observabilityConfig = this.observabilityConfig; + session.observabilityOptions_ = this.observabilityOptions_; return session; }); } @@ -2057,7 +2058,7 @@ class Database extends common.GrpcServiceObject { ? (optionsOrCallback as TimestampBounds) : {}; - const q = {opts: this.observabilityConfig}; + const q = {opts: this.observabilityOptions_}; return startTrace('Database.getSnapshot', q, span => { this.pool_.getSession((err, session) => { if (err) { @@ -2158,7 +2159,7 @@ class Database extends common.GrpcServiceObject { ? (optionsOrCallback as GetTransactionOptions) : {}; - const q = {opts: this.observabilityConfig}; + const q = {opts: this.observabilityOptions_}; return startTrace('Database.getTransaction', q, span => { this.pool_.getSession((err, session, transaction) => { if (options.requestOptions) { @@ -2785,7 +2786,7 @@ class Database extends common.GrpcServiceObject { ? (optionsOrCallback as TimestampBounds) : {}; - const q = {sql: query, opts: this.observabilityConfig}; + const q = {sql: query, opts: this.observabilityOptions_}; return startTrace('Database.run', q, span => { this.runStream(query, options) .on('error', err => { @@ -3006,7 +3007,7 @@ class Database extends common.GrpcServiceObject { options?: TimestampBounds ): PartialResultStream { const proxyStream: Transform = through.obj(); - const q = {sql: query, opts: this.observabilityConfig}; + const q = {sql: query, opts: this.observabilityOptions_}; return startTrace('Database.runStream', q, span => { this.pool_.getSession((err, session) => { if (err) { @@ -3184,7 +3185,7 @@ class Database extends common.GrpcServiceObject { ? (optionsOrRunFn as RunTransactionOptions) : {}; - const q = {opts: this.observabilityConfig}; + const q = {opts: this.observabilityOptions_}; startTrace('Database.runTransaction', q, span => { this.pool_.getSession((err, session?, transaction?) => { if (err) { @@ -3577,7 +3578,7 @@ class Database extends common.GrpcServiceObject { ? (optionsOrCallback as CallOptions) : {}; - const q = {opts: this.observabilityConfig}; + const q = {opts: this.observabilityOptions_}; return startTrace('Database.writeAtLeastOnce', q, span => { this.pool_.getSession((err, session?, transaction?) => { if (err && isSessionNotFoundError(err as grpc.ServiceError)) { diff --git a/src/index.ts b/src/index.ts index c13877694..39aa43e2d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2057,3 +2057,4 @@ import IInstanceConfig = instanceAdmin.spanner.admin.instance.v1.IInstanceConfig export {v1, protos}; export default {Spanner}; export {Float32, Float, Int, Struct, Numeric, PGNumeric, SpannerDate}; +export {ObservabilityOptions}; diff --git a/test/spanner.ts b/test/spanner.ts index 516a47124..2b49a7904 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -5015,7 +5015,7 @@ describe('Spanner with mock server', () => { const opts: typeof ObservabilityOptions = {tracerProvider: provider}; startTrace('aSpan', {opts: opts}, span => { const database = newTestDatabase(); - database.observabilityConfig = opts; + database.observabilityOptions_ = opts; async function runIt() { const query = { @@ -5054,70 +5054,6 @@ describe('Spanner with mock server', () => { ); }); }); - - it('Should use passed ObservabilityOptions in Spanner, Instance and Database', () => { - const exporter = new InMemorySpanExporter(); - const provider = new NodeTracerProvider({ - sampler: new AlwaysOnSampler(), - exporter: exporter, - }); - provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); - - const observabilityOptions: typeof ObservabilityOptions = { - tracerProvider: provider, - enableExtendedTracing: true, - }; - const spanner = new Spanner({ - servicePath: 'localhost', - port, - sslCreds: grpc.credentials.createInsecure(), - observabilityOptions: observabilityOptions, - }); - - // Ensure that the same observability configuration is set on the Spanner client. - assert.deepStrictEqual(spanner.observabilityOptions_, observabilityOptions); - - // Acquire a handle to the Instance through spanner.instance. - const instanceByHandle = spanner.instance('instance'); - assert.deepStrictEqual( - instanceByHandle.observabilityOptions_, - observabilityOptions - ); - - // Create the Instance by means of a constructor directly. - const instanceByConstructor = new Instance(spanner, 'myInstance'); - assert.deepStrictEqual( - instanceByConstructor.observabilityOptions_, - observabilityOptions - ); - - // Acquire a handle to the Database through instance.database. - const databaseByHandle = instanceByHandle.database('database'); - assert.deepStrictEqual( - databaseByHandle.observabilityOptions_, - observabilityOptions - ); - - // Create the Database by means of a constructor directly. - const databaseByConstructor = new Database( - instanceByConstructor, - 'myDatabase' - ); - assert.deepStrictEqual( - databaseByConstructor.observabilityOptions_, - observabilityOptions - ); - - spanner.close(); - - /* - * TODO: Once we've merged in end-to-end Database tests, - * we shall add another test in which we: - * Register a global exporter - * Inject a different exporter via Spanner - * Verify that injected exporter is used and not global. - */ - }); }); function executeSimpleUpdate(