Skip to content

Commit d60ff3a

Browse files
authored
feat(resource-detector-gcp)!: contribute Google's comprehensive resource detector (#3007)
1 parent f54a1ba commit d60ff3a

File tree

12 files changed

+1154
-314
lines changed

12 files changed

+1154
-314
lines changed

package-lock.json

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/resource-detector-gcp/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@
5252
"@types/mocha": "10.0.10",
5353
"@types/node": "18.18.14",
5454
"@types/semver": "7.5.8",
55-
"nock": "13.3.3",
55+
"@types/sinon": "17.0.4",
56+
"bignumber.js": "9.3.1",
5657
"nyc": "17.1.0",
57-
"rimraf": "5.0.10",
58+
"sinon": "15.2.0",
5859
"typescript": "5.0.4"
5960
},
6061
"peerDependencies": {

packages/resource-detector-gcp/src/detectors/GcpDetector.ts

Lines changed: 189 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/*
2+
* Copyright 2022 Google LLC
23
* Copyright The OpenTelemetry Authors
34
*
45
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,147 +15,216 @@
1415
* limitations under the License.
1516
*/
1617

17-
import * as gcpMetadata from 'gcp-metadata';
1818
import { context } from '@opentelemetry/api';
1919
import { suppressTracing } from '@opentelemetry/core';
2020
import {
21-
ResourceDetectionConfig,
22-
ResourceDetector,
23-
DetectedResource,
24-
DetectedResourceAttributes,
25-
} from '@opentelemetry/resources';
26-
import {
21+
CLOUDPLATFORMVALUES_GCP_APP_ENGINE,
22+
CLOUDPLATFORMVALUES_GCP_CLOUD_FUNCTIONS,
23+
CLOUDPLATFORMVALUES_GCP_CLOUD_RUN,
24+
CLOUDPLATFORMVALUES_GCP_COMPUTE_ENGINE,
25+
CLOUDPLATFORMVALUES_GCP_KUBERNETES_ENGINE,
2726
CLOUDPROVIDERVALUES_GCP,
2827
SEMRESATTRS_CLOUD_ACCOUNT_ID,
2928
SEMRESATTRS_CLOUD_AVAILABILITY_ZONE,
29+
SEMRESATTRS_CLOUD_PLATFORM,
3030
SEMRESATTRS_CLOUD_PROVIDER,
31-
SEMRESATTRS_CONTAINER_NAME,
31+
SEMRESATTRS_CLOUD_REGION,
32+
SEMRESATTRS_FAAS_INSTANCE,
33+
SEMRESATTRS_FAAS_NAME,
34+
SEMRESATTRS_FAAS_VERSION,
3235
SEMRESATTRS_HOST_ID,
3336
SEMRESATTRS_HOST_NAME,
37+
SEMRESATTRS_HOST_TYPE,
3438
SEMRESATTRS_K8S_CLUSTER_NAME,
35-
SEMRESATTRS_K8S_NAMESPACE_NAME,
36-
SEMRESATTRS_K8S_POD_NAME,
3739
} from '@opentelemetry/semantic-conventions';
3840

39-
/**
40-
* The GcpDetector can be used to detect if a process is running in the Google
41-
* Cloud Platform and return a {@link Resource} populated with metadata about
42-
* the instance. Returns an empty Resource if detection fails.
43-
*/
44-
class GcpDetector implements ResourceDetector {
45-
detect(_config?: ResourceDetectionConfig): DetectedResource {
46-
const attributes = context.with(suppressTracing(context.active()), () =>
47-
this._getAttributes()
48-
);
49-
return { attributes };
50-
}
41+
import { AttributeValue, Attributes } from '@opentelemetry/api';
42+
import {
43+
DetectedResource,
44+
DetectedResourceAttributes,
45+
emptyResource,
46+
Resource,
47+
ResourceDetector,
48+
resourceFromAttributes,
49+
} from '@opentelemetry/resources';
50+
import * as metadata from 'gcp-metadata';
51+
import * as faas from './faas';
52+
import * as gae from './gae';
53+
import * as gce from './gce';
54+
import * as gke from './gke';
5155

52-
/**
53-
* Asynchronously gather GCP cloud metadata.
54-
*/
55-
private _getAttributes(): DetectedResourceAttributes {
56-
const isAvail = gcpMetadata.isAvailable();
57-
58-
const attributes: DetectedResourceAttributes = {
59-
[SEMRESATTRS_CLOUD_PROVIDER]: (async () => {
60-
return (await isAvail) ? CLOUDPROVIDERVALUES_GCP : undefined;
61-
})(),
62-
[SEMRESATTRS_CLOUD_ACCOUNT_ID]: this._getProjectId(isAvail),
63-
[SEMRESATTRS_HOST_ID]: this._getInstanceId(isAvail),
64-
[SEMRESATTRS_HOST_NAME]: this._getHostname(isAvail),
65-
[SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: this._getZone(isAvail),
66-
};
67-
68-
// Add resource attributes for K8s.
69-
if (process.env.KUBERNETES_SERVICE_HOST) {
70-
attributes[SEMRESATTRS_K8S_CLUSTER_NAME] = this._getClusterName(isAvail);
71-
attributes[SEMRESATTRS_K8S_NAMESPACE_NAME] = (async () => {
72-
return (await isAvail) ? process.env.NAMESPACE : undefined;
73-
})();
74-
attributes[SEMRESATTRS_K8S_POD_NAME] = (async () => {
75-
return (await isAvail) ? process.env.HOSTNAME : undefined;
76-
})();
77-
attributes[SEMRESATTRS_CONTAINER_NAME] = (async () => {
78-
return (await isAvail) ? process.env.CONTAINER_NAME : undefined;
79-
})();
80-
}
81-
82-
return attributes;
83-
}
56+
const ATTRIBUTE_NAMES = [
57+
SEMRESATTRS_CLOUD_PLATFORM,
58+
SEMRESATTRS_CLOUD_AVAILABILITY_ZONE,
59+
SEMRESATTRS_CLOUD_REGION,
60+
SEMRESATTRS_K8S_CLUSTER_NAME,
61+
SEMRESATTRS_HOST_TYPE,
62+
SEMRESATTRS_HOST_ID,
63+
SEMRESATTRS_HOST_NAME,
64+
SEMRESATTRS_CLOUD_PROVIDER,
65+
SEMRESATTRS_CLOUD_ACCOUNT_ID,
66+
SEMRESATTRS_FAAS_NAME,
67+
SEMRESATTRS_FAAS_VERSION,
68+
SEMRESATTRS_FAAS_INSTANCE,
69+
] as const;
70+
71+
// Ensure that all resource keys are accounted for in ATTRIBUTE_NAMES
72+
type GcpResourceAttributeName = (typeof ATTRIBUTE_NAMES)[number];
73+
type GcpResourceAttributes = Partial<
74+
Record<GcpResourceAttributeName, AttributeValue>
75+
>;
8476

85-
/** Gets project id from GCP project metadata. */
86-
private async _getProjectId(
87-
isAvail: Promise<boolean>
88-
): Promise<string | undefined> {
89-
if (!(await isAvail)) {
90-
return undefined;
91-
}
92-
try {
93-
return await gcpMetadata.project('project-id');
94-
} catch {
95-
return '';
96-
}
77+
async function detect(): Promise<Resource> {
78+
if (!(await metadata.isAvailable())) {
79+
return emptyResource();
9780
}
9881

99-
/** Gets instance id from GCP instance metadata. */
100-
private async _getInstanceId(
101-
isAvail: Promise<boolean>
102-
): Promise<string | undefined> {
103-
if (!(await isAvail)) {
104-
return undefined;
105-
}
106-
try {
107-
const id = await gcpMetadata.instance('id');
108-
return id.toString();
109-
} catch {
110-
return '';
111-
}
82+
// Note the order of these if checks is significant with more specific resources coming
83+
// first. E.g. Cloud Functions gen2 are executed in Cloud Run so it must be checked first.
84+
if (await gke.onGke()) {
85+
return await gkeResource();
86+
} else if (await faas.onCloudFunctions()) {
87+
return await cloudFunctionsResource();
88+
} else if (await faas.onCloudRun()) {
89+
return await cloudRunResource();
90+
} else if (await gae.onAppEngine()) {
91+
return await gaeResource();
92+
} else if (await gce.onGce()) {
93+
return await gceResource();
11294
}
11395

114-
/** Gets zone from GCP instance metadata. */
115-
private async _getZone(
116-
isAvail: Promise<boolean>
117-
): Promise<string | undefined> {
118-
if (!(await isAvail)) {
119-
return undefined;
120-
}
121-
try {
122-
const zoneId = await gcpMetadata.instance('zone');
123-
if (zoneId) {
124-
return zoneId.split('/').pop();
125-
}
126-
return '';
127-
} catch {
128-
return '';
129-
}
96+
return emptyResource();
97+
}
98+
99+
async function gkeResource(): Promise<Resource> {
100+
const [zoneOrRegion, k8sClusterName, hostId] = await Promise.all([
101+
gke.availabilityZoneOrRegion(),
102+
gke.clusterName(),
103+
gke.hostId(),
104+
]);
105+
106+
return await makeResource({
107+
[SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_GCP_KUBERNETES_ENGINE,
108+
[zoneOrRegion.type === 'zone'
109+
? SEMRESATTRS_CLOUD_AVAILABILITY_ZONE
110+
: SEMRESATTRS_CLOUD_REGION]: zoneOrRegion.value,
111+
[SEMRESATTRS_K8S_CLUSTER_NAME]: k8sClusterName,
112+
[SEMRESATTRS_HOST_ID]: hostId,
113+
});
114+
}
115+
116+
async function cloudRunResource(): Promise<Resource> {
117+
const [faasName, faasVersion, faasInstance, faasCloudRegion] =
118+
await Promise.all([
119+
faas.faasName(),
120+
faas.faasVersion(),
121+
faas.faasInstance(),
122+
faas.faasCloudRegion(),
123+
]);
124+
125+
return await makeResource({
126+
[SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_GCP_CLOUD_RUN,
127+
[SEMRESATTRS_FAAS_NAME]: faasName,
128+
[SEMRESATTRS_FAAS_VERSION]: faasVersion,
129+
[SEMRESATTRS_FAAS_INSTANCE]: faasInstance,
130+
[SEMRESATTRS_CLOUD_REGION]: faasCloudRegion,
131+
});
132+
}
133+
134+
async function cloudFunctionsResource(): Promise<Resource> {
135+
const [faasName, faasVersion, faasInstance, faasCloudRegion] =
136+
await Promise.all([
137+
faas.faasName(),
138+
faas.faasVersion(),
139+
faas.faasInstance(),
140+
faas.faasCloudRegion(),
141+
]);
142+
143+
return await makeResource({
144+
[SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_GCP_CLOUD_FUNCTIONS,
145+
[SEMRESATTRS_FAAS_NAME]: faasName,
146+
[SEMRESATTRS_FAAS_VERSION]: faasVersion,
147+
[SEMRESATTRS_FAAS_INSTANCE]: faasInstance,
148+
[SEMRESATTRS_CLOUD_REGION]: faasCloudRegion,
149+
});
150+
}
151+
152+
async function gaeResource(): Promise<Resource> {
153+
let zone, region;
154+
if (await gae.onAppEngineStandard()) {
155+
[zone, region] = await Promise.all([
156+
gae.standardAvailabilityZone(),
157+
gae.standardCloudRegion(),
158+
]);
159+
} else {
160+
({ zone, region } = await gce.availabilityZoneAndRegion());
130161
}
162+
const [faasName, faasVersion, faasInstance] = await Promise.all([
163+
gae.serviceName(),
164+
gae.serviceVersion(),
165+
gae.serviceInstance(),
166+
]);
167+
168+
return await makeResource({
169+
[SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_GCP_APP_ENGINE,
170+
[SEMRESATTRS_FAAS_NAME]: faasName,
171+
[SEMRESATTRS_FAAS_VERSION]: faasVersion,
172+
[SEMRESATTRS_FAAS_INSTANCE]: faasInstance,
173+
[SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: zone,
174+
[SEMRESATTRS_CLOUD_REGION]: region,
175+
});
176+
}
131177

132-
/** Gets cluster name from GCP instance metadata. */
133-
private async _getClusterName(
134-
isAvail: Promise<boolean>
135-
): Promise<string | undefined> {
136-
if (!(await isAvail)) {
137-
return undefined;
138-
}
139-
try {
140-
return await gcpMetadata.instance('attributes/cluster-name');
141-
} catch {
142-
return '';
143-
}
178+
async function gceResource(): Promise<Resource> {
179+
const [zoneAndRegion, hostType, hostId, hostName] = await Promise.all([
180+
gce.availabilityZoneAndRegion(),
181+
gce.hostType(),
182+
gce.hostId(),
183+
gce.hostName(),
184+
]);
185+
186+
return await makeResource({
187+
[SEMRESATTRS_CLOUD_PLATFORM]: CLOUDPLATFORMVALUES_GCP_COMPUTE_ENGINE,
188+
[SEMRESATTRS_CLOUD_AVAILABILITY_ZONE]: zoneAndRegion.zone,
189+
[SEMRESATTRS_CLOUD_REGION]: zoneAndRegion.region,
190+
[SEMRESATTRS_HOST_TYPE]: hostType,
191+
[SEMRESATTRS_HOST_ID]: hostId,
192+
[SEMRESATTRS_HOST_NAME]: hostName,
193+
});
194+
}
195+
196+
async function makeResource(attrs: GcpResourceAttributes): Promise<Resource> {
197+
const project = await metadata.project<string>('project-id');
198+
199+
return resourceFromAttributes({
200+
[SEMRESATTRS_CLOUD_PROVIDER]: CLOUDPROVIDERVALUES_GCP,
201+
[SEMRESATTRS_CLOUD_ACCOUNT_ID]: project,
202+
...attrs,
203+
} satisfies GcpResourceAttributes);
204+
}
205+
206+
/**
207+
* Google Cloud resource detector which populates attributes based on the environment this
208+
* process is running in. If not on GCP, returns an empty resource.
209+
*/
210+
export class GcpDetector implements ResourceDetector {
211+
private async _asyncAttributes(): Promise<Attributes> {
212+
const resource = await context.with(
213+
suppressTracing(context.active()),
214+
detect
215+
);
216+
return resource.attributes;
144217
}
145218

146-
/** Gets hostname from GCP instance metadata. */
147-
private async _getHostname(
148-
isAvail: Promise<boolean>
149-
): Promise<string | undefined> {
150-
if (!(await isAvail)) {
151-
return undefined;
152-
}
153-
try {
154-
return await gcpMetadata.instance('hostname');
155-
} catch {
156-
return '';
157-
}
219+
detect(): DetectedResource {
220+
const asyncAttributes = this._asyncAttributes();
221+
const attributes = {} as DetectedResourceAttributes;
222+
ATTRIBUTE_NAMES.forEach(name => {
223+
// Each resource attribute is determined asynchronously in _gatherData().
224+
attributes[name] = asyncAttributes.then(data => data[name]);
225+
});
226+
227+
return { attributes };
158228
}
159229
}
160230

0 commit comments

Comments
 (0)