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..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, @@ -34,6 +36,10 @@ import { SEMRESATTRS_K8S_CLUSTER_NAME, SEMRESATTRS_K8S_NAMESPACE_NAME, SEMRESATTRS_K8S_POD_NAME, + SEMRESATTRS_FAAS_NAME, + SEMRESATTRS_FAAS_INSTANCE, + SEMRESATTRS_FAAS_VERSION, + SEMRESATTRS_CLOUD_PLATFORM, } from '@opentelemetry/semantic-conventions'; /** @@ -54,19 +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] = 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 7af4b8b374..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(() => { @@ -55,6 +91,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 +102,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 +221,47 @@ 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(); + + assert.strictEqual(resource.attributes[SEMRESATTRS_CLOUD_PLATFORM],CLOUDPLATFORMVALUES_GCP_CLOUD_RUN) + assertCloudResource(resource, { + provider: 'gcp', + accountId: 'my-project-id', + zone: 'my-zone', + }); + assertHostResource(resource, { + id: '4520031799277581759', + name: 'dev.my-project.local', + }); + assertFaasResource(resource, { + name: 'my-cloud-run-service', + version: 'my-cloud-run-revision', + instance: '4520031799277581759', + }) + }); }); });