Skip to content

Commit

Permalink
feat(resource-util): detect GKE and GCE resource attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
aabmass committed Dec 10, 2022
1 parent bf8edbb commit bf5631c
Show file tree
Hide file tree
Showing 9 changed files with 920 additions and 25 deletions.
410 changes: 385 additions & 25 deletions packages/opentelemetry-resource-util/package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions packages/opentelemetry-resource-util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,22 @@
"@opentelemetry/semantic-conventions": "1.8.0",
"@types/mocha": "9.1.1",
"@types/node": "14.18.34",
"@types/sinon": "^10.0.13",
"bignumber.js": "^9.1.1",
"codecov": "3.8.3",
"gts": "3.1.1",
"mocha": "9.2.2",
"nyc": "15.1.0",
"sinon": "^14.0.2",
"snap-shot-it": "7.9.7",
"ts-mocha": "9.0.2",
"typescript": "4.8.4"
},
"peerDependencies": {
"@opentelemetry/resources": "^1.0.0",
"@opentelemetry/semantic-conventions": "^1.0.0"
},
"dependencies": {
"gcp-metadata": "^5.0.1"
}
}
69 changes: 69 additions & 0 deletions packages/opentelemetry-resource-util/src/detector/detector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {
SemanticResourceAttributes as Semconv,
CloudPlatformValues,
} from '@opentelemetry/semantic-conventions';

import {Detector, Resource} from '@opentelemetry/resources';
import * as gce from './gce';
import * as gke from './gke';

export class GcpDetector implements Detector {
async detect(): Promise<Resource> {
if (await gke.onGke()) {
return await this._gkeResource();
} else if (await gce.onGce()) {
return await this._gceResource();
}

return Resource.EMPTY;
}

private async _gkeResource(): Promise<Resource> {
const [zoneOrRegion, k8sClusterName, hostId] = await Promise.all([
gke.availabilityZoneOrRegion(),
gke.clusterName(),
gke.hostId(),
]);

return new Resource({
[Semconv.CLOUD_PLATFORM]: CloudPlatformValues.GCP_KUBERNETES_ENGINE,
[zoneOrRegion.type === 'zone'
? Semconv.CLOUD_AVAILABILITY_ZONE
: Semconv.CLOUD_REGION]: zoneOrRegion.value,
[Semconv.K8S_CLUSTER_NAME]: k8sClusterName,
[Semconv.HOST_ID]: hostId,
});
}

private async _gceResource(): Promise<Resource> {
const [zoneAndRegion, hostType, hostId, hostName] = await Promise.all([
gce.availabilityZoneAndRegion(),
gce.hostType(),
gce.hostId(),
gce.hostName(),
]);

return new Resource({
[Semconv.CLOUD_PLATFORM]: CloudPlatformValues.GCP_COMPUTE_ENGINE,
[Semconv.CLOUD_AVAILABILITY_ZONE]: zoneAndRegion.zone,
[Semconv.CLOUD_REGION]: zoneAndRegion.region,
[Semconv.HOST_TYPE]: hostType,
[Semconv.HOST_ID]: hostId,
[Semconv.HOST_NAME]: hostName,
});
}
}
90 changes: 90 additions & 0 deletions packages/opentelemetry-resource-util/src/detector/gce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Implementation in this file copied from
* https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/gce.go
*/

import {diag} from '@opentelemetry/api';
import * as metadata from 'gcp-metadata';

const MACHINE_TYPE_METADATA_ATTR = 'machine-type';
const ID_METADATA_ATTR = 'id';
const HOSTNAME_METADATA_ATTR = 'hostname';
const ZONE_METADATA_ATTR = 'zone';

export async function onGce(): Promise<boolean> {
try {
await metadata.instance<string>(MACHINE_TYPE_METADATA_ATTR);
return true;
} catch (err) {
diag.debug(
'Could not fetch metadata attribute %s, assuming not on GCE. Error was %s',
MACHINE_TYPE_METADATA_ATTR,
err
);
return false;
}
}

/**
* The machine type of the instance on which this program is running. Check that {@link
* onGce()} is true before calling this, or it may throw exceptions.
*/
export async function hostType(): Promise<string> {
return metadata.instance<string>(MACHINE_TYPE_METADATA_ATTR);
}

/**
* The instance ID of the instance on which this program is running. Check that {@link onGce()}
* is true before calling this, or it may throw exceptions.
*/
export async function hostId(): Promise<string> {
// May be a bignumber.js BigNumber which can just be converted with toString(). See
// https://github.com/googleapis/gcp-metadata#take-care-with-large-number-valued-properties
const id = await metadata.instance<number | Object>(ID_METADATA_ATTR);
return id.toString();
}

/**
* The instance ID of the instance on which this program is running. Check that {@link onGce()}
* is true before calling this, or it may throw exceptions.
*/
export async function hostName(): Promise<string> {
return metadata.instance<string>(HOSTNAME_METADATA_ATTR);
}

/**
* The zone and region in which this program is running. Check that {@link onGce()} is true
* before calling this, or it may throw exceptions.
*/
export async function availabilityZoneAndRegion(): Promise<{
zone: string;
region: string;
}> {
const fullZone = await metadata.instance<string>(ZONE_METADATA_ATTR);

// Format described in
// https://cloud.google.com/compute/docs/metadata/default-metadata-values#vm_instance_metadata
const re = /projects\/\d+\/zones\/(?<zone>(?<region>\w+-\w+)-\w+)/;
const {zone, region} = fullZone.match(re)?.groups ?? {};
if (!zone || !region) {
throw new Error(
`zone was not in the expected format: projects/PROJECT_NUM/zones/COUNTRY-REGION-ZONE. Got ${fullZone}`
);
}

return {zone, region};
}
78 changes: 78 additions & 0 deletions packages/opentelemetry-resource-util/src/detector/gke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Implementation in this file copied from
* https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/detectors/gcp/gke.go
*/

import * as metadata from 'gcp-metadata';
import * as gce from './gce';

const KUBERNETES_SERVICE_HOST_ENV = 'KUBERNETES_SERVICE_HOST';
const CLUSTER_NAME_METADATA_ATTR = 'cluster-name';
const CLUSTER_LOCATION_METADATA_ATTR = 'cluster-location';

export async function onGke(): Promise<boolean> {
return process.env[KUBERNETES_SERVICE_HOST_ENV] !== undefined;
}

/**
* The instance ID of the instance on which this program is running. Check that {@link onGke()}
* is true before calling this, or it may throw exceptions.
*/
export async function hostId(): Promise<string> {
return await gce.hostId();
}

/**
* The name of the GKE cluster in which this program is running. Check that {@link onGke()} is
* true before calling this, or it may throw exceptions.
*/
export async function clusterName(): Promise<string> {
return metadata.instance<string>(CLUSTER_NAME_METADATA_ATTR);
}

/**
* The location of the cluster and whether the cluster is zonal or regional. Check that {@link
* onGke()} is true before calling this, or it may throw exceptions.
*/
export async function availabilityZoneOrRegion(): Promise<{
type: 'zone' | 'region';
value: string;
}> {
const clusterLocation = await metadata.instance<string>(
CLUSTER_LOCATION_METADATA_ATTR
);
switch (countChar(clusterLocation, '-')) {
case 1:
return {type: 'region', value: clusterLocation};
case 2:
return {type: 'zone', value: clusterLocation};
default:
throw new Error(
`unrecognized format for cluster location: ${clusterLocation}`
);
}
}

function countChar(s: string, char: string): number {
let count = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === char) {
count += 1;
}
}
return count;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as sinon from 'sinon';
import * as metadata from 'gcp-metadata';

import {GcpDetector} from '../../src/detector/detector';
import * as assert from 'assert';

describe('GcpDetector', () => {
let metadataStub: sinon.SinonStubbedInstance<typeof metadata>;
let envStub: NodeJS.ProcessEnv;
beforeEach(() => {
metadataStub = sinon.stub(metadata);
envStub = sinon.replace(process, 'env', {});
});

afterEach(() => {
sinon.restore();
});

describe('detects a GKE resource', () => {
beforeEach(() => {
envStub.KUBERNETES_SERVICE_HOST = 'fake-service-host';
metadataStub.instance
.withArgs('id')
.resolves(12345)

.withArgs('cluster-name')
.resolves('fake-cluster-name');
});

it('zonal', async () => {
metadataStub.instance.withArgs('cluster-location').resolves('us-east4-b');
const resource = await new GcpDetector().detect();
assert.deepStrictEqual(resource.attributes, {
'cloud.availability_zone': 'us-east4-b',
'cloud.platform': 'gcp_kubernetes_engine',
'host.id': '12345',
'k8s.cluster.name': 'fake-cluster-name',
});
});

it('regional', async () => {
metadataStub.instance.withArgs('cluster-location').resolves('us-east4');
const resource = await new GcpDetector().detect();
assert.deepStrictEqual(resource.attributes, {
'cloud.region': 'us-east4',
'cloud.platform': 'gcp_kubernetes_engine',
'host.id': '12345',
'k8s.cluster.name': 'fake-cluster-name',
});
});
});

it('detects a GCE resource', async () => {
metadataStub.instance
.withArgs('id')
.resolves(12345)

.withArgs('machine-type')
.resolves('fake-machine-type')

.withArgs('hostname')
.resolves('fake-hostname')

.withArgs('zone')
.resolves('projects/233510669999/zones/us-east4-b');

const resource = await new GcpDetector().detect();
assert.deepStrictEqual(resource.attributes, {
'cloud.availability_zone': 'us-east4-b',
'cloud.platform': 'gcp_compute_engine',
'cloud.region': 'us-east4',
'host.id': '12345',
'host.name': 'fake-hostname',
'host.type': 'fake-machine-type',
});
});

it('detects empty resource when nothing else can be detected', async () => {
// gcp-metadata throws when it can't access the metadata server
metadataStub.instance.rejects();

const resource = await new GcpDetector().detect();
assert.deepStrictEqual(resource.attributes, {});
});
});
Loading

0 comments on commit bf5631c

Please sign in to comment.