From 44433fa073f81c687bbc8d01703a0fb9173caace Mon Sep 17 00:00:00 2001 From: Kasper Borg Nissen Date: Fri, 9 May 2025 13:28:25 +0200 Subject: [PATCH 1/2] feat(gcp-detector): add Cloud Run support with faas.* (#1) * feat(gcp-detector): add Cloud Run support with faas.name, faas.version, and faas.instance Signed-off-by: Kasper Borg Nissen * chore(lint): remove newlines Signed-off-by: Kasper Borg Nissen * chore(comment): write comment on moving the assertions * test(gcp-detector): fix Cloud Run tests --------- Signed-off-by: Kasper Borg Nissen --- .../src/detectors/GcpDetector.ts | 10 +++ .../test/detectors/GcpDetector.test.ts | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts b/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts index 3a5aa06021..e833aedb62 100644 --- a/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts @@ -34,6 +34,9 @@ import { SEMRESATTRS_K8S_CLUSTER_NAME, SEMRESATTRS_K8S_NAMESPACE_NAME, SEMRESATTRS_K8S_POD_NAME, + SEMRESATTRS_FAAS_NAME, + SEMRESATTRS_FAAS_INSTANCE, + SEMRESATTRS_FAAS_VERSION, } from '@opentelemetry/semantic-conventions'; /** @@ -65,6 +68,13 @@ class GcpDetector implements ResourceDetector { [SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: this._getZone(isAvail), }; + // Add resource attributes for Cloud Run. + if (process.env.K_SERVICE) { + attributes[SEMRESATTRS_FAAS_NAME] = process.env.K_SERVICE; + attributes[SEMRESATTRS_FAAS_VERSION] = process.env.K_REVISION; + attributes[SEMRESATTRS_FAAS_INSTANCE] = this._getInstanceId(isAvail); + } + // Add resource attributes for K8s. if (process.env.KUBERNETES_SERVICE_HOST) { attributes[SEMRESATTRS_K8S_CLUSTER_NAME] = this._getClusterName(isAvail); diff --git a/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts b/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts index 7af4b8b374..bc61ddd0a1 100644 --- a/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts @@ -55,6 +55,8 @@ describe('gcpDetector', () => { delete process.env.NAMESPACE; delete process.env.CONTAINER_NAME; delete process.env.HOSTNAME; + delete process.env.K_SERVICE; + delete process.env.K_REVISION; }); beforeEach(() => { @@ -64,6 +66,8 @@ describe('gcpDetector', () => { delete process.env.NAMESPACE; delete process.env.CONTAINER_NAME; delete process.env.HOSTNAME; + delete process.env.K_SERVICE; + delete process.env.K_REVISION; }); it('should return resource with GCP metadata', async () => { @@ -181,5 +185,65 @@ describe('gcpDetector', () => { await resource.waitForAsyncAttributes?.(); assertEmptyResource(resource); }); + + it('should populate Cloud Run attributes when K_SERVICE is set', async () => { + process.env.K_SERVICE = 'my-cloud-run-service'; + process.env.K_REVISION = 'my-cloud-run-revision'; + + const scope = nock(HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS) + .get(INSTANCE_ID_PATH) + .reply(200, () => '4520031799277581759', HEADERS) + .get(PROJECT_ID_PATH) + .reply(200, () => 'my-project-id', HEADERS) + .get(ZONE_PATH) + .reply(200, () => 'project/zone/my-zone', HEADERS) + .get(HOSTNAME_PATH) + .reply(200, () => 'dev.my-project.local', HEADERS); + const secondaryScope = nock(SECONDARY_HOST_ADDRESS) + .get(INSTANCE_PATH) + .reply(200, {}, HEADERS); + + const resource = detectResources({ detectors: [gcpDetector] }); + await resource.waitForAsyncAttributes?.(); + + secondaryScope.done(); + scope.done(); + + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertHostResource(resource, { + id: '4520031799277581759', + name: 'dev.my-project.local', + }); + + const attrs = resource.attributes; + + // This should be moved to the @opentelemetry/contrib-test-utils and replaced once available. + // Check faas.name and faas.version which are simple string values + if (attrs['faas.name'] !== 'my-cloud-run-service') { + throw new Error(`Cloud Run faas.name is "${attrs['faas.name']}" instead of "my-cloud-run-service"`); + } + + if (attrs['faas.version'] !== 'my-cloud-run-revision') { + throw new Error(`Cloud Run faas.version is "${attrs['faas.version']}" instead of "my-cloud-run-revision"`); + } + + // For faas.instance, it could be a resolved value or a Promise + if (attrs['faas.instance'] instanceof Promise) { + const resolvedInstance = await attrs['faas.instance']; + if (resolvedInstance !== '4520031799277581759') { + throw new Error(`Cloud Run faas.instance resolved to "${resolvedInstance}" instead of "4520031799277581759"`); + } + } else if (attrs['faas.instance'] !== '' && attrs['faas.instance'] !== '4520031799277581759') { + // The current implementation is returning an empty string, but the correct value would be the instance ID + // We accept either for test compatibility + throw new Error(`Cloud Run faas.instance is "${attrs['faas.instance']}" which is not empty or the instance ID`); + } + }).timeout(3000); }); }); From 4ed64cc1b54fff25d0e2028a4d3eb11b5c1d4307 Mon Sep 17 00:00:00 2001 From: Kasper Borg Nissen Date: Tue, 13 May 2025 12:38:33 +0200 Subject: [PATCH 2/2] chore(gcp-detector): clean up code and test case for cloud run support (#2) Signed-off-by: Kasper Borg Nissen --- .../src/detectors/GcpDetector.ts | 12 +++- .../test/detectors/GcpDetector.test.ts | 72 ++++++++++++------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts b/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts index e833aedb62..b9918695ef 100644 --- a/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts +++ b/detectors/node/opentelemetry-resource-detector-gcp/src/detectors/GcpDetector.ts @@ -24,6 +24,8 @@ import { DetectedResourceAttributes, } from '@opentelemetry/resources'; import { + CLOUDPLATFORMVALUES_GCP_KUBERNETES_ENGINE, + CLOUDPLATFORMVALUES_GCP_CLOUD_RUN, CLOUDPROVIDERVALUES_GCP, SEMRESATTRS_CLOUD_ACCOUNT_ID, SEMRESATTRS_CLOUD_AVAILABILITY_ZONE, @@ -37,6 +39,7 @@ import { SEMRESATTRS_FAAS_NAME, SEMRESATTRS_FAAS_INSTANCE, SEMRESATTRS_FAAS_VERSION, + SEMRESATTRS_CLOUD_PLATFORM, } from '@opentelemetry/semantic-conventions'; /** @@ -57,26 +60,29 @@ class GcpDetector implements ResourceDetector { */ private _getAttributes(): DetectedResourceAttributes { const isAvail = gcpMetadata.isAvailable(); - + const instanceId = this._getInstanceId(isAvail); + const attributes: DetectedResourceAttributes = { [SEMRESATTRS_CLOUD_PROVIDER]: (async () => { return (await isAvail) ? CLOUDPROVIDERVALUES_GCP : undefined; })(), [SEMRESATTRS_CLOUD_ACCOUNT_ID]: this._getProjectId(isAvail), - [SEMRESATTRS_HOST_ID]: this._getInstanceId(isAvail), + [SEMRESATTRS_HOST_ID]: instanceId, [SEMRESATTRS_HOST_NAME]: this._getHostname(isAvail), [SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: this._getZone(isAvail), }; // Add resource attributes for Cloud Run. if (process.env.K_SERVICE) { + attributes[SEMRESATTRS_CLOUD_PLATFORM] = CLOUDPLATFORMVALUES_GCP_CLOUD_RUN; attributes[SEMRESATTRS_FAAS_NAME] = process.env.K_SERVICE; attributes[SEMRESATTRS_FAAS_VERSION] = process.env.K_REVISION; - attributes[SEMRESATTRS_FAAS_INSTANCE] = this._getInstanceId(isAvail); + attributes[SEMRESATTRS_FAAS_INSTANCE] = instanceId; } // Add resource attributes for K8s. if (process.env.KUBERNETES_SERVICE_HOST) { + attributes[SEMRESATTRS_CLOUD_PLATFORM] = CLOUDPLATFORMVALUES_GCP_KUBERNETES_ENGINE; attributes[SEMRESATTRS_K8S_CLUSTER_NAME] = this._getClusterName(isAvail); attributes[SEMRESATTRS_K8S_NAMESPACE_NAME] = (async () => { return (await isAvail) ? process.env.NAMESPACE : undefined; diff --git a/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts b/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts index bc61ddd0a1..0a36524ea9 100644 --- a/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts +++ b/detectors/node/opentelemetry-resource-detector-gcp/test/detectors/GcpDetector.test.ts @@ -31,7 +31,15 @@ import { assertContainerResource, assertEmptyResource, } from '@opentelemetry/contrib-test-utils'; -import { detectResources } from '@opentelemetry/resources'; +import { detectResources, Resource } from '@opentelemetry/resources'; +import * as assert from 'assert'; +import { + CLOUDPLATFORMVALUES_GCP_CLOUD_RUN, + SEMRESATTRS_CLOUD_PLATFORM, + SEMRESATTRS_FAAS_INSTANCE, + SEMRESATTRS_FAAS_NAME, + SEMRESATTRS_FAAS_VERSION, +} from '@opentelemetry/semantic-conventions'; const HEADERS = { [HEADER_NAME.toLowerCase()]: HEADER_VALUE, @@ -43,6 +51,34 @@ const ZONE_PATH = BASE_PATH + '/instance/zone'; const CLUSTER_NAME_PATH = BASE_PATH + '/instance/attributes/cluster-name'; const HOSTNAME_PATH = BASE_PATH + '/instance/hostname'; +const assertFaasResource = ( + resource: Resource, + validations: { + name?: string; + instance?: string; + version?: string; + } +) => { + if (validations.name) { + assert.strictEqual( + resource.attributes[SEMRESATTRS_FAAS_NAME], + validations.name + ); + } + if (validations.instance) { + assert.strictEqual( + resource.attributes[SEMRESATTRS_FAAS_INSTANCE], + validations.instance + ); + } + if (validations.version) { + assert.strictEqual( + resource.attributes[SEMRESATTRS_FAAS_VERSION], + validations.version + ); + } +}; + describe('gcpDetector', () => { describe('.detect', () => { before(() => { @@ -210,7 +246,8 @@ describe('gcpDetector', () => { secondaryScope.done(); scope.done(); - + + assert.strictEqual(resource.attributes[SEMRESATTRS_CLOUD_PLATFORM],CLOUDPLATFORMVALUES_GCP_CLOUD_RUN) assertCloudResource(resource, { provider: 'gcp', accountId: 'my-project-id', @@ -220,30 +257,11 @@ describe('gcpDetector', () => { id: '4520031799277581759', name: 'dev.my-project.local', }); - - const attrs = resource.attributes; - - // This should be moved to the @opentelemetry/contrib-test-utils and replaced once available. - // Check faas.name and faas.version which are simple string values - if (attrs['faas.name'] !== 'my-cloud-run-service') { - throw new Error(`Cloud Run faas.name is "${attrs['faas.name']}" instead of "my-cloud-run-service"`); - } - - if (attrs['faas.version'] !== 'my-cloud-run-revision') { - throw new Error(`Cloud Run faas.version is "${attrs['faas.version']}" instead of "my-cloud-run-revision"`); - } - - // For faas.instance, it could be a resolved value or a Promise - if (attrs['faas.instance'] instanceof Promise) { - const resolvedInstance = await attrs['faas.instance']; - if (resolvedInstance !== '4520031799277581759') { - throw new Error(`Cloud Run faas.instance resolved to "${resolvedInstance}" instead of "4520031799277581759"`); - } - } else if (attrs['faas.instance'] !== '' && attrs['faas.instance'] !== '4520031799277581759') { - // The current implementation is returning an empty string, but the correct value would be the instance ID - // We accept either for test compatibility - throw new Error(`Cloud Run faas.instance is "${attrs['faas.instance']}" which is not empty or the instance ID`); - } - }).timeout(3000); + assertFaasResource(resource, { + name: 'my-cloud-run-service', + version: 'my-cloud-run-revision', + instance: '4520031799277581759', + }) + }); }); });