|
1 | 1 | /*
|
| 2 | + * Copyright 2022 Google LLC |
2 | 3 | * Copyright The OpenTelemetry Authors
|
3 | 4 | *
|
4 | 5 | * Licensed under the Apache License, Version 2.0 (the "License");
|
|
14 | 15 | * limitations under the License.
|
15 | 16 | */
|
16 | 17 |
|
17 |
| -import * as gcpMetadata from 'gcp-metadata'; |
18 | 18 | import { context } from '@opentelemetry/api';
|
19 | 19 | import { suppressTracing } from '@opentelemetry/core';
|
20 | 20 | 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, |
27 | 26 | CLOUDPROVIDERVALUES_GCP,
|
28 | 27 | SEMRESATTRS_CLOUD_ACCOUNT_ID,
|
29 | 28 | SEMRESATTRS_CLOUD_AVAILABILITY_ZONE,
|
| 29 | + SEMRESATTRS_CLOUD_PLATFORM, |
30 | 30 | SEMRESATTRS_CLOUD_PROVIDER,
|
31 |
| - SEMRESATTRS_CONTAINER_NAME, |
| 31 | + SEMRESATTRS_CLOUD_REGION, |
| 32 | + SEMRESATTRS_FAAS_INSTANCE, |
| 33 | + SEMRESATTRS_FAAS_NAME, |
| 34 | + SEMRESATTRS_FAAS_VERSION, |
32 | 35 | SEMRESATTRS_HOST_ID,
|
33 | 36 | SEMRESATTRS_HOST_NAME,
|
| 37 | + SEMRESATTRS_HOST_TYPE, |
34 | 38 | SEMRESATTRS_K8S_CLUSTER_NAME,
|
35 |
| - SEMRESATTRS_K8S_NAMESPACE_NAME, |
36 |
| - SEMRESATTRS_K8S_POD_NAME, |
37 | 39 | } from '@opentelemetry/semantic-conventions';
|
38 | 40 |
|
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'; |
51 | 55 |
|
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 | +>; |
84 | 76 |
|
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(); |
97 | 80 | }
|
98 | 81 |
|
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(); |
112 | 94 | }
|
113 | 95 |
|
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()); |
130 | 161 | }
|
| 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 | +} |
131 | 177 |
|
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; |
144 | 217 | }
|
145 | 218 |
|
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 }; |
158 | 228 | }
|
159 | 229 | }
|
160 | 230 |
|
|
0 commit comments