diff --git a/core/packages/gax/src/clientInterface.ts b/core/packages/gax/src/clientInterface.ts index b9f3bd8b8e9..bf4571d6268 100644 --- a/core/packages/gax/src/clientInterface.ts +++ b/core/packages/gax/src/clientInterface.ts @@ -29,9 +29,7 @@ import * as longrunning from './longRunningCalls/longrunning'; import * as operationProtos from '../protos/operations'; export interface ClientOptions - extends GrpcClientOptions, - GoogleAuthOptions, - ClientStubOptions { + extends GrpcClientOptions, GoogleAuthOptions, ClientStubOptions { libName?: string; libVersion?: string; clientConfig?: gax.ClientConfig; @@ -42,6 +40,10 @@ 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; } export interface Descriptors { diff --git a/core/packages/gax/src/createApiCall.ts b/core/packages/gax/src/createApiCall.ts index b3fdee987a6..e161879d5c9 100644 --- a/core/packages/gax/src/createApiCall.ts +++ b/core/packages/gax/src/createApiCall.ts @@ -66,8 +66,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 +153,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 +168,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..33f8d1672d3 100644 --- a/core/packages/gax/src/gax.ts +++ b/core/packages/gax/src/gax.ts @@ -170,6 +170,7 @@ export interface CallOptions { longrunning?: BackoffSettings; apiName?: string; retryRequestOptions?: RetryRequestOptions; + enableTelemetryTracing?: boolean; } export class CallSettings { @@ -186,6 +187,7 @@ export class CallSettings { longrunning?: BackoffSettings; apiName?: string; retryRequestOptions?: RetryRequestOptions; + enableTelemetryTracing?: boolean; /** * @param {Object} settings - An object containing parameters of this settings. @@ -219,6 +221,7 @@ export class CallSettings { 'longrunning' in settings ? settings.longrunning : undefined; this.apiName = settings.apiName ?? undefined; this.retryRequestOptions = settings.retryRequestOptions; + this.enableTelemetryTracing = settings.enableTelemetryTracing; } /** @@ -242,6 +245,7 @@ export class CallSettings { let longrunning = this.longrunning; let apiName = this.apiName; let retryRequestOptions = this.retryRequestOptions; + let enableTelemetryTracing = this.enableTelemetryTracing; // If the user provides a timeout to the method, that timeout value will be used // to override the backoff settings. @@ -297,6 +301,9 @@ export class CallSettings { if ('retryRequestOptions' in options) { retryRequestOptions = options.retryRequestOptions; } + if ('enableTelemetryTracing' in options) { + enableTelemetryTracing = options.enableTelemetryTracing; + } return new CallSettings({ timeout, @@ -309,6 +316,7 @@ export class CallSettings { isBundling, apiName, retryRequestOptions, + enableTelemetryTracing, }); } } diff --git a/core/packages/gax/src/index.ts b/core/packages/gax/src/index.ts index d9c82f34530..55c13393378 100644 --- a/core/packages/gax/src/index.ts +++ b/core/packages/gax/src/index.ts @@ -113,7 +113,13 @@ export { PaginationResponse, } from './clientInterface'; -export {makeUUID, decodeAnyProtosInArray, decodeProtobufAny} from './util'; +export { + makeUUID, + decodeAnyProtosInArray, + decodeProtobufAny, + checkTelemetryEnabled, + StaticTraceContext, +} from './util'; export {ServiceError, ChannelCredentials} from '@grpc/grpc-js'; export {warn} from './warnings'; diff --git a/core/packages/gax/src/util.ts b/core/packages/gax/src/util.ts index 16f32f56eb7..0492a4321c2 100644 --- a/core/packages/gax/src/util.ts +++ b/core/packages/gax/src/util.ts @@ -14,12 +14,38 @@ * limitations under the License. */ +import {CallSettings} from './gax'; + const PROTO_TYPE_PREFIX = 'type.googleapis.com/'; const NUM_OF_PARTS_IN_PROTO_TYPE_NAME = 2; const randomUUID = () => globalThis.crypto?.randomUUID() || require('crypto').randomUUID(); +/** + * 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; +} + +/** + * 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?.otherArgs?.internalTelemetryInfo !== undefined; + return Boolean(tracingEnabled); +} + function words(str: string, normalize = false) { if (normalize) { // strings like somethingABCSomething are special case for protobuf.js, diff --git a/core/packages/gax/test/unit/apiCallable.ts b/core/packages/gax/test/unit/apiCallable.ts index f341ec12f01..f7cfaed4148 100644 --- a/core/packages/gax/test/unit/apiCallable.ts +++ b/core/packages/gax/test/unit/apiCallable.ts @@ -329,6 +329,31 @@ describe('createApiCall', () => { ); } }); + + describe('in regards to OpenTelemetry Tracing', () => { + afterEach(() => { + delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + }); + + 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 mockCallOptions: gax.CallOptions = { + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: { + gcpClientService: 'test.googleapis.com', + }, + }, + }; + const apiCall = createApiCall(() => {}, {settings: mockCallOptions}); + 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 +375,7 @@ describe('Promise', () => { assert.ok(Array.isArray(response)); assert.strictEqual(response[0], 42); assert.ok(deadlineArg); - done(); + return done(); }) .catch(done); }); @@ -380,7 +405,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 +445,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 +534,7 @@ describe('retryable', () => { assert.strictEqual(resp[0], 1729); assert.strictEqual(toAttempt, 0); assert.ok(deadlineArg); - done(); + return done(); }) .catch(done); }); @@ -534,7 +559,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 +820,7 @@ describe('retryable', () => { }) .then(() => { mockBuilder.verify(); + return; }); }); @@ -821,9 +847,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/exports.ts b/core/packages/gax/test/unit/exports.ts index 8132209b46d..fefa004f9fd 100644 --- a/core/packages/gax/test/unit/exports.ts +++ b/core/packages/gax/test/unit/exports.ts @@ -62,6 +62,9 @@ describe('exports', () => { it('exports protobufMinimal', () => { assert(typeof index.protobufMinimal === 'object'); }); + it('exports checkTelemetryEnabled', () => { + assert(typeof index.checkTelemetryEnabled === 'function'); + }); }); describe('fallback', () => { diff --git a/core/packages/gax/test/unit/gax.ts b/core/packages/gax/test/unit/gax.ts index 214b3d07d46..d166ef98c03 100644 --- a/core/packages/gax/test/unit/gax.ts +++ b/core/packages/gax/test/unit/gax.ts @@ -198,4 +198,39 @@ describe('gax construct settings', () => { assert.strictEqual(backoff.maxRetryDelayMillis, 1000); assert.deepStrictEqual(settings.retry.retryCodes, [RETRY_DICT.code_c]); }); + + describe('CallSettings telemetry fields', () => { + it('defaults enableTelemetryTracing to undefined', () => { + const settings = new gax.CallSettings(); + assert.strictEqual(settings.enableTelemetryTracing, undefined); + }); + + it('initializes enableTelemetryTracing', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + }); + assert.strictEqual(settings.enableTelemetryTracing, true); + }); + + it('merges enableTelemetryTracing', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + }); + const merged = settings.merge({ + enableTelemetryTracing: false, + }); + assert.strictEqual(merged.enableTelemetryTracing, false); + }); + + it('copies enableTelemetryTracing when merging with null/empty options', () => { + const settings = new gax.CallSettings({ + enableTelemetryTracing: true, + }); + const mergedNull = settings.merge(null); + assert.strictEqual(mergedNull.enableTelemetryTracing, true); + + const mergedEmpty = settings.merge({}); + assert.strictEqual(mergedEmpty.enableTelemetryTracing, true); + }); + }); }); diff --git a/core/packages/gax/test/unit/util.ts b/core/packages/gax/test/unit/util.ts index 2a716fa2711..85be595496b 100644 --- a/core/packages/gax/test/unit/util.ts +++ b/core/packages/gax/test/unit/util.ts @@ -15,7 +15,7 @@ */ import assert from 'assert'; -import {describe, it} from 'mocha'; +import {afterEach, describe, it} from 'mocha'; import { toCamelCase as snakeToCamelCase, camelToSnakeCase, @@ -24,7 +24,10 @@ import { getProtoNameFromFullName, decodeProtobufAny, decodeAnyProtosInArray, + checkTelemetryEnabled, + StaticTraceContext, } from '../../src/util'; +import {CallSettings} from '../../src/gax'; import * as protobuf from 'protobufjs'; import protosJson from '../../protos/status.json'; @@ -196,4 +199,63 @@ describe('util.ts', () => { JSON.stringify([{reason: 'SERVICE_DISABLED'}]), ); }); + + describe('checkTelemetryEnabled', () => { + afterEach(() => { + delete process.env.GOOGLE_SDK_NODE_EXPERIMENTAL_O11Y_ENABLED; + }); + + const mockTelemetryInfo: StaticTraceContext = { + gcpClientService: 'test.googleapis.com', + gcpVersion: '1.0.0', + gcpRepo: 'googleapis/google-cloud-node', + gcpArtifact: 'google-cloud-test', + }; + + const mockSettings = new CallSettings({ + enableTelemetryTracing: true, + otherArgs: { + internalTelemetryInfo: mockTelemetryInfo, + }, + }); + + 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 CallSettings({ + otherArgs: { + 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 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); + }); + }); });