diff --git a/core/packages/gax/src/clientInterface.ts b/core/packages/gax/src/clientInterface.ts index b9f3bd8b8e9..b872b795c33 100644 --- a/core/packages/gax/src/clientInterface.ts +++ b/core/packages/gax/src/clientInterface.ts @@ -28,10 +28,19 @@ import { import * as longrunning from './longRunningCalls/longrunning'; import * as operationProtos from '../protos/operations'; +/** + * The static trace context is information about the Google Cloud client library that is + * used to generate telemetry tracing information. + */ +export interface StaticTraceContext { + gcpClientService?: string; + gcpVersion?: string; + gcpRepo?: string; + gcpArtifact?: string; +} + export interface ClientOptions - extends GrpcClientOptions, - GoogleAuthOptions, - ClientStubOptions { + extends GrpcClientOptions, GoogleAuthOptions, ClientStubOptions { libName?: string; libVersion?: string; clientConfig?: gax.ClientConfig; @@ -42,6 +51,15 @@ export interface ClientOptions // No preference; exception will be thrown if both are set to different values. universeDomain?: string; universe_domain?: string; + /** + * Whether to enable telemetry tracing for the client. + */ + enableTelemetryTracing?: boolean; + /** + * The static trace context is information about the Google Cloud client library that is + * used to generate telemetry tracing information. + */ + internalTelemetryInfo?: StaticTraceContext; } export interface Descriptors { diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index b3fdee987a6..ca0e66a9aa1 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -34,6 +34,19 @@ import {addTimeoutArg} from './normalCalls/timeout'; import {StreamingApiCaller} from './streamingCalls/streamingApiCaller'; import {warn} from './warnings'; +/** + * Checks if telemetry tracing is enabled + * @param settings + * @returns true if telemetry tracing is enabled, false otherwise + */ +export function checkTelemetryEnabled(settings?: CallSettings): boolean { + const tracingEnabled = + Boolean(settings?.enableTelemetryTracing) && + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED === 'true' && + settings?.internalTelemetryInfo !== undefined; + return Boolean(tracingEnabled); +} + /** * Converts an rpc call into an API call governed by the settings. * @@ -66,8 +79,7 @@ export function createApiCall( const funcPromise = typeof func === 'function' ? Promise.resolve(func) : func; // the following apiCaller will be used for all calls of this function... const apiCaller = createAPICaller(settings, descriptor); - - return ( + const invokeCall = ( request: RequestType, callOptions?: CallOptions, callback?: APICallback, @@ -154,7 +166,12 @@ export function createApiCall( .then((apiCall: SimpleCallbackFunction) => { // After adding retries / timeouts, the call function becomes simpler: // it only accepts request and callback. - currentApiCaller.call(apiCall, request, thisSettings, ongoingCall); + return currentApiCaller.call( + apiCall, + request, + thisSettings, + ongoingCall, + ); }) .catch(err => { currentApiCaller.fail(ongoingCall, err); @@ -164,4 +181,5 @@ export function createApiCall( // or to cancel the ongoing call. return currentApiCaller.result(ongoingCall); }; + return invokeCall; } diff --git a/core/packages/gax/src/gax.ts b/core/packages/gax/src/gax.ts index a38152c2429..8b70f5d9b05 100644 --- a/core/packages/gax/src/gax.ts +++ b/core/packages/gax/src/gax.ts @@ -25,6 +25,7 @@ import {BundleOptions} from './bundlingCalls/bundleExecutor'; import {toLowerCamelCase} from './util'; import {Status} from './status'; import {RequestType} from './apitypes'; +import type {StaticTraceContext} from './clientInterface'; /** * Encapsulates the overridable settings for a particular API call. @@ -170,6 +171,8 @@ export interface CallOptions { longrunning?: BackoffSettings; apiName?: string; retryRequestOptions?: RetryRequestOptions; + enableTelemetryTracing?: boolean; + internalTelemetryInfo?: StaticTraceContext; } export class CallSettings { @@ -186,6 +189,8 @@ export class CallSettings { longrunning?: BackoffSettings; apiName?: string; retryRequestOptions?: RetryRequestOptions; + enableTelemetryTracing?: boolean; + internalTelemetryInfo?: StaticTraceContext; /** * @param {Object} settings - An object containing parameters of this settings. @@ -219,6 +224,8 @@ export class CallSettings { 'longrunning' in settings ? settings.longrunning : undefined; this.apiName = settings.apiName ?? undefined; this.retryRequestOptions = settings.retryRequestOptions; + this.enableTelemetryTracing = settings.enableTelemetryTracing; + this.internalTelemetryInfo = settings.internalTelemetryInfo; } /** @@ -242,6 +249,8 @@ export class CallSettings { let longrunning = this.longrunning; let apiName = this.apiName; let retryRequestOptions = this.retryRequestOptions; + let enableTelemetryTracing = this.enableTelemetryTracing; + let internalTelemetryInfo = this.internalTelemetryInfo; // If the user provides a timeout to the method, that timeout value will be used // to override the backoff settings. @@ -297,6 +306,12 @@ export class CallSettings { if ('retryRequestOptions' in options) { retryRequestOptions = options.retryRequestOptions; } + if ('enableTelemetryTracing' in options) { + enableTelemetryTracing = options.enableTelemetryTracing; + } + if ('internalTelemetryInfo' in options) { + internalTelemetryInfo = options.internalTelemetryInfo; + } return new CallSettings({ timeout, @@ -309,6 +324,8 @@ export class CallSettings { isBundling, apiName, retryRequestOptions, + enableTelemetryTracing, + internalTelemetryInfo, }); } } diff --git a/core/packages/gax/src/index.ts b/core/packages/gax/src/index.ts index d9c82f34530..710d90e42a7 100644 --- a/core/packages/gax/src/index.ts +++ b/core/packages/gax/src/index.ts @@ -29,7 +29,7 @@ export * as loggingUtils from 'google-logging-utils'; export {grpc}; export {CancellablePromise, OngoingCall} from './call'; -export {createApiCall} from './createApiCall'; +export {createApiCall, checkTelemetryEnabled} from './createApiCall'; export { BundleDescriptor, LongrunningDescriptor, diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index f341ec12f01..59bed189bad 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -21,6 +21,7 @@ import * as sinon from 'sinon'; import {RequestType} from '../../src/apitypes'; import * as gax from '../../src/gax'; +import {checkTelemetryEnabled} from '../../src/createApiCall'; import {GoogleError} from '../../src/googleError'; import * as utils from './utils'; import * as retries from '../../src/normalCalls/retries'; @@ -329,6 +330,75 @@ describe('createApiCall', () => { ); } }); + + describe('in regards to OpenTelemetry Tracing', () => { + afterEach(() => { + delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + }); + + describe('checkTelemetryEnabled', () => { + const mockSettings = new gax.CallSettings({ + enableTelemetryTracing: true, + internalTelemetryInfo: { + gcpClientService: 'test.googleapis.com', + }, + }); + + it('returns true when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED=true and settings are configured', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + assert.strictEqual(checkTelemetryEnabled(mockSettings), true); + }); + + it('returns false when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED is not set', () => { + assert.strictEqual(checkTelemetryEnabled(mockSettings), false); + }); + + it('returns false when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED is not "true"', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'false'; + assert.strictEqual(checkTelemetryEnabled(mockSettings), false); + }); + + it('returns false when enableTelemetryTracing is not set on settings', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const noTracingSettings = new gax.CallSettings({ + internalTelemetryInfo: { + gcpClientService: 'test.googleapis.com', + }, + }); + assert.strictEqual(checkTelemetryEnabled(noTracingSettings), false); + }); + + it('returns false when internalTelemetryInfo is not set on settings', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const noInfoSettings = new gax.CallSettings({ + enableTelemetryTracing: true, + }); + assert.strictEqual(checkTelemetryEnabled(noInfoSettings), false); + }); + + it('returns false when settings is undefined', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + assert.strictEqual(checkTelemetryEnabled(undefined), false); + }); + }); + + it('creates an api call when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED and CallSettings field is set', () => { + process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED = 'true'; + const mockCallSettings = { + enableTelemetryTracing: true, + internalTelemetryInfo: { + gcpClientService: 'test.googleapis.com', + }, + }; + const apiCall = createApiCall(() => {}, {settings: mockCallSettings}); + assert.strictEqual(typeof apiCall, 'function'); + }); + + it('creates an api call when GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED is not set', () => { + const apiCall = createApiCall(() => {}); + assert.strictEqual(typeof apiCall, 'function'); + }); + }); }); describe('Promise', () => { @@ -350,7 +420,7 @@ describe('Promise', () => { assert.ok(Array.isArray(response)); assert.strictEqual(response[0], 42); assert.ok(deadlineArg); - done(); + return done(); }) .catch(done); }); @@ -380,7 +450,7 @@ describe('Promise', () => { const promise = (apiCall as any)(null); promise .then(() => { - done(new Error('should not reach')); + return done(new Error('should not reach')); }) .catch((err: {code: number}) => { assert(err instanceof GoogleError); @@ -420,7 +490,7 @@ describe('Promise', () => { const promise = (apiCall as any)(null); promise .then(() => { - done(new Error('should not reach')); + return done(new Error('should not reach')); }) .catch(() => { assert(callCount < 4); @@ -509,7 +579,7 @@ describe('retryable', () => { assert.strictEqual(resp[0], 1729); assert.strictEqual(toAttempt, 0); assert.ok(deadlineArg); - done(); + return done(); }) .catch(done); }); @@ -534,7 +604,7 @@ describe('retryable', () => { const promise = apiCall({}, undefined); promise .then(() => { - done(new Error('should not reach')); + return done(new Error('should not reach')); }) .catch((err: Error) => { assert(err instanceof Error); @@ -795,6 +865,7 @@ describe('retryable', () => { }) .then(() => { mockBuilder.verify(); + return; }); }); @@ -821,9 +892,9 @@ describe('retryable', () => { try { assert.strictEqual(gotHeaders.h1, 'val1'); assert.strictEqual(gotHeaders.h2, 'val2'); - done(); + return done(); } catch (err) { - done(err); + return done(err); } }); }); diff --git a/core/packages/gax/test/unit/gax.ts b/core/packages/gax/test/unit/gax.ts index 214b3d07d46..74271d8df16 100644 --- a/core/packages/gax/test/unit/gax.ts +++ b/core/packages/gax/test/unit/gax.ts @@ -198,4 +198,75 @@ describe('gax construct settings', () => { assert.strictEqual(backoff.maxRetryDelayMillis, 1000); assert.deepStrictEqual(settings.retry.retryCodes, [RETRY_DICT.code_c]); }); + + describe('CallSettings telemetry fields', () => { + const mockTelemetryInfo = { + gcpClientService: 'test.googleapis.com', + gcpVersion: '1.0.0', + gcpRepo: 'googleapis/google-cloud-node', + gcpArtifact: 'google-cloud-test', + }; + + it('defaults enableTelemetryTracing and internalTelemetryInfo to undefined', () => { + const settings = new gax.CallSettings(); + assert.strictEqual(settings.enableTelemetryTracing, undefined); + assert.strictEqual(settings.internalTelemetryInfo, undefined); + }); + + it('initializes enableTelemetryTracing and internalTelemetryInfo', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + internalTelemetryInfo: mockTelemetryInfo, + }); + assert.strictEqual(settings.enableTelemetryTracing, true); + assert.deepStrictEqual(settings.internalTelemetryInfo, mockTelemetryInfo); + }); + + it('merges enableTelemetryTracing and internalTelemetryInfo', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + internalTelemetryInfo: mockTelemetryInfo, + }); + const merged = settings.merge({ + enableTelemetryTracing: false, + }); + assert.strictEqual(merged.enableTelemetryTracing, false); + assert.deepStrictEqual(merged.internalTelemetryInfo, mockTelemetryInfo); + }); + + it('merges with new internalTelemetryInfo', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + internalTelemetryInfo: mockTelemetryInfo, + }); + const newTelemetryInfo = { + gcpClientService: 'updated.googleapis.com', + }; + const merged = settings.merge({ + internalTelemetryInfo: newTelemetryInfo, + }); + assert.strictEqual(merged.enableTelemetryTracing, true); + assert.deepStrictEqual(merged.internalTelemetryInfo, newTelemetryInfo); + }); + + it('copies telemetry fields when merging with null/empty options', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + internalTelemetryInfo: mockTelemetryInfo, + }); + const mergedNull = settings.merge(null); + assert.strictEqual(mergedNull.enableTelemetryTracing, true); + assert.deepStrictEqual( + mergedNull.internalTelemetryInfo, + mockTelemetryInfo, + ); + + const mergedEmpty = settings.merge({}); + assert.strictEqual(mergedEmpty.enableTelemetryTracing, true); + assert.deepStrictEqual( + mergedEmpty.internalTelemetryInfo, + mockTelemetryInfo, + ); + }); + }); });