diff --git a/src/agent/debuglet.ts b/src/agent/debuglet.ts index 9e7b8e44..ff4718b9 100644 --- a/src/agent/debuglet.ts +++ b/src/agent/debuglet.ts @@ -472,6 +472,12 @@ export class Debuglet extends EventEmitter { that.logger.warn(NODE_10_CIRC_REF_MESSAGE); } + const platform = Debuglet.getPlatform(); + let region: string | undefined; + if (platform === Platforms.CLOUD_FUNCTION) { + region = await Debuglet.getRegion(); + } + // We can register as a debuggee now. that.logger.debug('Starting debuggee, project', project); that.running = true; @@ -484,9 +490,12 @@ export class Debuglet extends EventEmitter { sourceContext, onGCP, that.debug.packageInfo, + platform, that.config.description, - undefined + /*errorMessage=*/ undefined, + region ); + that.scheduleRegistration_(0 /* immediately */); that.emit('started'); } @@ -534,8 +543,10 @@ export class Debuglet extends EventEmitter { sourceContext: SourceContext | undefined, onGCP: boolean, packageInfo: PackageInfo, + platform: string, description?: string, - errorMessage?: string + errorMessage?: string, + region?: string ): Debuggee { const cwd = process.cwd(); const mainScript = path.relative(cwd, process.argv[1]); @@ -555,9 +566,13 @@ export class Debuglet extends EventEmitter { 'agent.name': packageInfo.name, 'agent.version': packageInfo.version, projectid: projectId, - platform: Debuglet.getPlatform(), + platform, }; + if (region) { + labels.region = region; + } + if (serviceContext) { if ( typeof serviceContext.service === 'string' && @@ -580,6 +595,10 @@ export class Debuglet extends EventEmitter { } } + if (region) { + desc += ' region:' + region; + } + if (!description && process.env.FUNCTION_NAME) { description = 'Function: ' + process.env.FUNCTION_NAME; } @@ -620,7 +639,7 @@ export class Debuglet extends EventEmitter { * Use environment vars to infer the current platform. * For now this is only Cloud Functions and other. */ - private static getPlatform(): Platforms { + static getPlatform(): Platforms { const {FUNCTION_NAME, FUNCTION_TARGET} = process.env; // (In theory) only the Google Cloud Functions environment will have these env vars. if (FUNCTION_NAME || FUNCTION_TARGET) { @@ -637,6 +656,27 @@ export class Debuglet extends EventEmitter { return (await metadata.instance('attributes/cluster-name')).data as string; } + /** + * Returns the region from environment varaible if available. + * Otherwise, returns the region from the metadata service. + * If metadata is not available, returns undefined. + */ + static async getRegion(): Promise { + if (process.env.FUNCTION_REGION) { + return process.env.FUNCTION_REGION; + } + + try { + // Example returned region format: /process/1234567/us-central + const segments = ((await metadata.instance('region')) as string).split( + '/' + ); + return segments[segments.length - 1]; + } catch (err) { + return undefined; + } + } + static async getSourceContextFromFile(): Promise { // If read errors, the error gets thrown to the caller. const contents = await readFilep('source-context.json', 'utf8'); diff --git a/test/test-debuglet.ts b/test/test-debuglet.ts index 17656f8d..4439861f 100644 --- a/test/test-debuglet.ts +++ b/test/test-debuglet.ts @@ -856,6 +856,80 @@ describe('Debuglet', () => { debuglet.start(); }); + it('should attempt to retreive region correctly if needed', done => { + const savedGetPlatform = Debuglet.getPlatform; + Debuglet.getPlatform = () => Platforms.CLOUD_FUNCTION; + + const clusterScope = nock(gcpMetadata.HOST_ADDRESS) + .get('/computeMetadata/v1/instance/region') + .once() + .reply(200, '123/456/region_name', gcpMetadata.HEADERS); + + const debug = new Debug( + {projectId: 'fake-project', credentials: fakeCredentials}, + packageInfo + ); + + nocks.oauth2(); + + const config = debugletConfig(); + const debuglet = new Debuglet(debug, config); + const scope = nock(config.apiUrl) + .post(REGISTER_PATH) + .reply(200, { + debuggee: {id: DEBUGGEE_ID}, + }); + + debuglet.once('registered', () => { + Debuglet.getPlatform = savedGetPlatform; + assert.strictEqual( + (debuglet.debuggee as Debuggee).labels?.region, + 'region_name' + ); + debuglet.stop(); + clusterScope.done(); + scope.done(); + done(); + }); + + debuglet.start(); + }); + + it('should continue to register when could not get region', done => { + const savedGetPlatform = Debuglet.getPlatform; + Debuglet.getPlatform = () => Platforms.CLOUD_FUNCTION; + + const clusterScope = nock(gcpMetadata.HOST_ADDRESS) + .get('/computeMetadata/v1/instance/region') + .once() + .reply(400); + + const debug = new Debug( + {projectId: 'fake-project', credentials: fakeCredentials}, + packageInfo + ); + + nocks.oauth2(); + + const config = debugletConfig(); + const debuglet = new Debuglet(debug, config); + const scope = nock(config.apiUrl) + .post(REGISTER_PATH) + .reply(200, { + debuggee: {id: DEBUGGEE_ID}, + }); + + debuglet.once('registered', () => { + Debuglet.getPlatform = savedGetPlatform; + debuglet.stop(); + clusterScope.done(); + scope.done(); + done(); + }); + + debuglet.start(); + }); + it('should pass config source context to api', done => { const REPO_URL = 'https://github.com/non-existent-users/non-existend-repo'; @@ -1462,7 +1536,8 @@ describe('Debuglet', () => { {service: 'some-service', version: 'production'}, {}, false, - packageInfo + packageInfo, + Platforms.DEFAULT ); assert.ok(debuggee); assert.ok(debuggee.labels); @@ -1477,7 +1552,8 @@ describe('Debuglet', () => { {service: 'default', version: 'yellow.5'}, {}, false, - packageInfo + packageInfo, + Platforms.DEFAULT ); assert.ok(debuggee); assert.ok(debuggee.labels); @@ -1493,6 +1569,7 @@ describe('Debuglet', () => { {}, false, packageInfo, + Platforms.DEFAULT, undefined, 'Some Error Message' ); @@ -1507,7 +1584,8 @@ describe('Debuglet', () => { {enableCanary: true, allowCanaryOverride: true}, {}, false, - packageInfo + packageInfo, + Platforms.DEFAULT ); assert.strictEqual(debuggee.canaryMode, 'CANARY_MODE_DEFAULT_ENABLED'); }); @@ -1519,7 +1597,8 @@ describe('Debuglet', () => { {enableCanary: true, allowCanaryOverride: false}, {}, false, - packageInfo + packageInfo, + Platforms.DEFAULT ); assert.strictEqual(debuggee.canaryMode, 'CANARY_MODE_ALWAYS_ENABLED'); }); @@ -1531,7 +1610,8 @@ describe('Debuglet', () => { {enableCanary: false, allowCanaryOverride: true}, {}, false, - packageInfo + packageInfo, + Platforms.DEFAULT ); assert.strictEqual(debuggee.canaryMode, 'CANARY_MODE_DEFAULT_DISABLED'); }); @@ -1543,54 +1623,65 @@ describe('Debuglet', () => { {enableCanary: false, allowCanaryOverride: false}, {}, false, - packageInfo + packageInfo, + Platforms.DEFAULT ); assert.strictEqual(debuggee.canaryMode, 'CANARY_MODE_ALWAYS_DISABLED'); }); + }); + describe('getPlatform', () => { it('should correctly identify default platform.', () => { - const debuggee = Debuglet.createDebuggee( - 'some project', - 'id', - {service: 'some-service', version: 'production'}, - {}, - false, - packageInfo - ); - assert.ok(debuggee.labels!.platform === Platforms.DEFAULT); + assert.ok(Debuglet.getPlatform() === Platforms.DEFAULT); }); it('should correctly identify GCF (legacy) platform.', () => { // GCF sets this env var on older runtimes. process.env.FUNCTION_NAME = 'mock'; - const debuggee = Debuglet.createDebuggee( - 'some project', - 'id', - {service: 'some-service', version: 'production'}, - {}, - false, - packageInfo - ); - assert.ok(debuggee.labels!.platform === Platforms.CLOUD_FUNCTION); + assert.ok(Debuglet.getPlatform() === Platforms.CLOUD_FUNCTION); delete process.env.FUNCTION_NAME; // Don't contaminate test environment. }); it('should correctly identify GCF (modern) platform.', () => { // GCF sets this env var on modern runtimes. process.env.FUNCTION_TARGET = 'mock'; - const debuggee = Debuglet.createDebuggee( - 'some project', - 'id', - {service: 'some-service', version: 'production'}, - {}, - false, - packageInfo - ); - assert.ok(debuggee.labels!.platform === Platforms.CLOUD_FUNCTION); + assert.ok(Debuglet.getPlatform() === Platforms.CLOUD_FUNCTION); delete process.env.FUNCTION_TARGET; // Don't contaminate test environment. }); }); + describe('getRegion', () => { + it('should return function region for older GCF runtime', async () => { + process.env.FUNCTION_REGION = 'mock'; + + assert.ok((await Debuglet.getRegion()) === 'mock'); + + delete process.env.FUNCTION_REGION; + }); + + it('should return region for newer GCF runtime', async () => { + const clusterScope = nock(gcpMetadata.HOST_ADDRESS) + .get('/computeMetadata/v1/instance/region') + .once() + .reply(200, '123/456/region_name', gcpMetadata.HEADERS); + + assert.ok((await Debuglet.getRegion()) === 'region_name'); + + clusterScope.done(); + }); + + it('should return undefined when cannot get region metadata', async () => { + const clusterScope = nock(gcpMetadata.HOST_ADDRESS) + .get('/computeMetadata/v1/instance/region') + .once() + .reply(400); + + assert.ok((await Debuglet.getRegion()) === undefined); + + clusterScope.done(); + }); + }); + describe('_createUniquifier', () => { it('should create a unique string', () => { const fn = Debuglet._createUniquifier;