Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 4daea53

Browse files
authored
Stats/Stackdriver: Use resource util to generate MonitoredResource (#314)
* Exporter/Stats/Stackdriver: Use resource util to generate MonitoredResource * fix review comments
1 parent 9414faf commit 4daea53

File tree

8 files changed

+186
-22
lines changed

8 files changed

+186
-22
lines changed

packages/opencensus-exporter-stackdriver/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
},
5454
"dependencies": {
5555
"@opencensus/core": "^0.0.8",
56+
"@opencensus/resource-util": "^0.0.1",
5657
"google-auth-library": "^1.5.0",
5758
"googleapis": "27.0.0",
5859
"hex2dec": "^1.1.0"

packages/opencensus-exporter-stackdriver/src/stackdriver-monitoring.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@
1717
import {logger, Logger, Measurement, Metric, MetricDescriptor as OCMetricDescriptor, MetricProducerManager, Metrics, StatsEventListener, TagKey, TagValue, View} from '@opencensus/core';
1818
import {auth, JWT} from 'google-auth-library';
1919
import {google} from 'googleapis';
20-
21-
import {createMetricDescriptorData, createTimeSeriesList} from './stackdriver-stats-utils';
22-
import {StackdriverExporterOptions, TimeSeries} from './types';
20+
import {createMetricDescriptorData, createTimeSeriesList, getDefaultResource} from './stackdriver-stats-utils';
21+
import {MonitoredResource, StackdriverExporterOptions, TimeSeries} from './types';
2322

2423
google.options({headers: {'x-opencensus-outgoing-request': 0x1}});
2524
const monitoring = google.monitoring('v3');
@@ -36,11 +35,10 @@ export class StackdriverStatsExporter implements StatsEventListener {
3635
static readonly DEFAULT_DISPLAY_NAME_PREFIX: string = 'OpenCensus';
3736
static readonly CUSTOM_OPENCENSUS_DOMAIN: string =
3837
'custom.googleapis.com/opencensus';
39-
static readonly GLOBAL: string = 'global';
4038
static readonly PERIOD: number = 60000;
4139
private registeredMetricDescriptors: Map<string, OCMetricDescriptor> =
4240
new Map();
43-
41+
private DEFAULT_RESOURCE: Promise<MonitoredResource>;
4442
logger: Logger;
4543

4644
constructor(options: StackdriverExporterOptions) {
@@ -54,6 +52,7 @@ export class StackdriverStatsExporter implements StatsEventListener {
5452
options.prefix || StackdriverStatsExporter.DEFAULT_DISPLAY_NAME_PREFIX;
5553
this.logger = options.logger || logger.logger();
5654
this.onMetricUploadError = options.onMetricUploadError;
55+
this.DEFAULT_RESOURCE = getDefaultResource(this.projectId);
5756
}
5857

5958
/**
@@ -132,11 +131,9 @@ export class StackdriverStatsExporter implements StatsEventListener {
132131
* be uploaded to StackDriver.
133132
* @param metricsList The List of Metric.
134133
*/
135-
private createTimeSeries(metricsList: Metric[]) {
134+
private async createTimeSeries(metricsList: Metric[]) {
136135
const timeSeries: TimeSeries[] = [];
137-
const resourceLabels = {project_id: this.projectId};
138-
const monitoredResource = {type: 'global', labels: resourceLabels};
139-
136+
const monitoredResource = await this.DEFAULT_RESOURCE;
140137
for (const metric of metricsList) {
141138
timeSeries.push(...createTimeSeriesList(
142139
metric, monitoredResource, this.metricPrefix));

packages/opencensus-exporter-stackdriver/src/stackdriver-stats-utils.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {BucketOptions, DistributionBucket, DistributionValue, LabelKey, LabelValue, Metric, MetricDescriptor as OCMetricDescriptor, MetricDescriptorType, TimeSeriesPoint, Timestamp} from '@opencensus/core';
17+
import {BucketOptions, DistributionBucket, DistributionValue, LabelKey, Labels, LabelValue, Metric, MetricDescriptor as OCMetricDescriptor, MetricDescriptorType, TimeSeriesPoint, Timestamp} from '@opencensus/core';
18+
import * as resource from '@opencensus/resource-util';
1819
import * as os from 'os';
1920
import * as path from 'path';
2021

@@ -23,6 +24,26 @@ import {Distribution, LabelDescriptor, MetricDescriptor, MetricKind, MonitoredRe
2324
const OPENCENSUS_TASK = 'opencensus_task';
2425
const OPENCENSUS_TASK_DESCRIPTION = 'Opencensus task identifier';
2526
export const OPENCENSUS_TASK_VALUE_DEFAULT = generateDefaultTaskValue();
27+
const STACKDRIVER_PROJECT_ID_KEY = 'project_id';
28+
29+
/* Return a self-configured StackDriver monitored resource. */
30+
export async function getDefaultResource(projectId: string):
31+
Promise<MonitoredResource> {
32+
const labels: Labels = {project_id: projectId};
33+
const autoDetectedResource = await resource.detectResource();
34+
const [type, mappings] = getTypeAndMappings(autoDetectedResource.type);
35+
Object.keys(mappings).forEach((key) => {
36+
if (autoDetectedResource.labels[mappings[key]]) {
37+
if (mappings[key] === resource.AWS_REGION_KEY) {
38+
labels[key] = `${resource.AWS_REGION_VALUE_PREFIX}${
39+
autoDetectedResource.labels[mappings[key]]}`;
40+
} else {
41+
labels[key] = autoDetectedResource.labels[mappings[key]];
42+
}
43+
}
44+
});
45+
return {type, labels};
46+
}
2647

2748
/** Converts a OpenCensus MetricDescriptor to a StackDriver MetricDescriptor. */
2849
export function createMetricDescriptorData(
@@ -237,6 +258,44 @@ function leftZeroPad(ns: number) {
237258
return `${pad}${str}`;
238259
}
239260

261+
function getTypeAndMappings(resourceType: string): [string, Labels] {
262+
switch (resourceType) {
263+
case resource.GCP_GCE_INSTANCE_TYPE:
264+
// https://cloud.google.com/monitoring/api/resources#tag_gce_instance
265+
return [
266+
'gce_instance', {
267+
'project_id': STACKDRIVER_PROJECT_ID_KEY,
268+
'instance_id': resource.GCP_INSTANCE_ID_KEY,
269+
'zone': resource.GCP_ZONE_KEY
270+
}
271+
];
272+
case resource.K8S_CONTAINER_TYPE:
273+
// https://cloud.google.com/monitoring/api/resources#tag_k8s_container
274+
return [
275+
'k8s_container', {
276+
'project_id': STACKDRIVER_PROJECT_ID_KEY,
277+
'location': resource.GCP_ZONE_KEY,
278+
'cluster_name': resource.K8S_CLUSTER_NAME_KEY,
279+
'namespace_name': resource.K8S_NAMESPACE_NAME_KEY,
280+
'pod_name': resource.K8S_POD_NAME_KEY,
281+
'container_name': resource.K8S_CONTAINER_NAME_KEY
282+
}
283+
];
284+
case resource.AWS_EC2_INSTANCE_TYPE:
285+
// https://cloud.google.com/monitoring/api/resources#tag_aws_ec2_instance
286+
return [
287+
'aws_ec2_instance', {
288+
'project_id': STACKDRIVER_PROJECT_ID_KEY,
289+
'instance_id': resource.AWS_INSTANCE_ID_KEY,
290+
'region': resource.AWS_REGION_KEY,
291+
'aws_account': resource.AWS_ACCOUNT_KEY
292+
}
293+
];
294+
default:
295+
return ['global', {}];
296+
}
297+
}
298+
240299
export const TEST_ONLY = {
241300
createMetricType,
242301
createDisplayName,

packages/opencensus-exporter-stackdriver/test/nocks.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import * as nock from 'nock';
2121

2222
const accept = () => true;
2323

24+
const HEADERS = {
25+
['metadata-flavor']: 'Google'
26+
};
27+
2428
export function oauth2<T extends {} = {}>(validator?: (body: T) => boolean):
2529
nock.Scope {
2630
validator = validator || accept;
@@ -45,6 +49,31 @@ export function projectId(status: number|(() => string), reply?: () => string) {
4549
.reply(status, reply, {'Metadata-Flavor': 'Google'});
4650
}
4751

52+
export function noDetectResource() {
53+
const scopes = [
54+
nock('http://metadata.google.internal')
55+
.get('/computeMetadata/v1/instance')
56+
.once()
57+
.replyWithError({code: 'ENOTFOUND'}),
58+
nock('http://169.254.169.254/latest/dynamic/instance-identity/document')
59+
.get('')
60+
.replyWithError({code: 'ENOTFOUND'})
61+
];
62+
return scopes;
63+
}
64+
65+
export function detectGceResource() {
66+
return nock('http://metadata.google.internal')
67+
.get('/computeMetadata/v1/instance')
68+
.reply(200, {}, HEADERS)
69+
.get('/computeMetadata/v1/project/project-id')
70+
.reply(200, () => 'my-project-id', HEADERS)
71+
.get('/computeMetadata/v1/instance/zone')
72+
.reply(200, () => 'project/zone/my-zone', HEADERS)
73+
.get('/computeMetadata/v1/instance/id')
74+
.reply(200, () => 4520031799277581759, HEADERS);
75+
}
76+
4877
export function instanceId(
4978
status: number|(() => string), reply?: () => string) {
5079
if (typeof status === 'function') {
@@ -118,4 +147,4 @@ export function metricDescriptors<T extends {} = {}>(
118147
scope = interceptor.reply(200, reply);
119148
}
120149
return scope;
121-
}
150+
}

packages/opencensus-exporter-stackdriver/test/test-stackdriver-monitoring.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import {globalStats, LabelKey, Logger, MeasureUnit, Metrics} from '@opencensus/core';
1818
import * as assert from 'assert';
19-
19+
import * as nock from 'nock';
2020
import {StackdriverStatsExporter} from '../src/stackdriver-monitoring';
2121
import {MetricKind, StackdriverExporterOptions, ValueType} from '../src/types';
2222

@@ -65,8 +65,8 @@ describe('Stackdriver Stats Exporter', () => {
6565

6666
before(() => {
6767
exporterOptions = {period: 0, projectId: PROJECT_ID, logger: mockLogger};
68+
nocks.noDetectResource();
6869
exporter = new StackdriverStatsExporter(exporterOptions);
69-
7070
nocks.oauth2();
7171
});
7272

packages/opencensus-exporter-stackdriver/test/test-stackdriver-stats-utils.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {DistributionValue, LabelKey, LabelValue, MetricDescriptor as OCMetricDescriptor, MetricDescriptorType, TimeSeriesPoint, Timestamp} from '@opencensus/core';
17+
import {CoreResource, DistributionValue, LabelKey, LabelValue, MetricDescriptor as OCMetricDescriptor, MetricDescriptorType, TimeSeriesPoint, Timestamp} from '@opencensus/core';
1818
import * as assert from 'assert';
1919

2020
import {StackdriverStatsExporter} from '../src/stackdriver-monitoring';
21-
import {createMetricDescriptorData, createTimeSeriesList, OPENCENSUS_TASK_VALUE_DEFAULT, TEST_ONLY} from '../src/stackdriver-stats-utils';
21+
import {createMetricDescriptorData, createTimeSeriesList, getDefaultResource, OPENCENSUS_TASK_VALUE_DEFAULT, TEST_ONLY} from '../src/stackdriver-stats-utils';
2222
import {Distribution, MetricDescriptor, MetricKind, ValueType} from '../src/types';
2323

24+
import * as nocks from './nocks';
25+
2426
const METRIC_NAME = 'metric-name';
2527
const METRIC_DESCRIPTION = 'metric-description';
2628
const DEFAULT_UNIT = '1';
@@ -413,4 +415,76 @@ describe('Stackdriver Stats Exporter Utils', () => {
413415
assert.deepStrictEqual(point2.value, {doubleValue: 15});
414416
});
415417
});
418+
419+
describe('getDefaultResource()', () => {
420+
beforeEach(() => {
421+
delete process.env.OC_RESOURCE_TYPE;
422+
delete process.env.OC_RESOURCE_LABELS;
423+
CoreResource.setup();
424+
});
425+
426+
it('should return a global MonitoredResource', async () => {
427+
nocks.noDetectResource();
428+
const monitoredResource = await getDefaultResource('my-project-id');
429+
const {type, labels} = monitoredResource;
430+
431+
assert.equal(type, 'global');
432+
assert.deepEqual(labels, {project_id: 'my-project-id'});
433+
});
434+
435+
it('should return a k8s MonitoredResource', async () => {
436+
process.env.OC_RESOURCE_TYPE = 'k8s.io/container';
437+
process.env.OC_RESOURCE_LABELS = 'k8s.io/pod/name=pod-xyz-123,' +
438+
'k8s.io/container/name=c1,k8s.io/namespace/name=default,' +
439+
'cloud.google.com/gce/zone=zone1';
440+
CoreResource.setup();
441+
const monitoredResource = await getDefaultResource('my-project-id');
442+
const {type, labels} = monitoredResource;
443+
444+
assert.equal(type, 'k8s_container');
445+
assert.equal(Object.keys(labels).length, 5);
446+
assert.deepStrictEqual(labels, {
447+
'container_name': 'c1',
448+
'namespace_name': 'default',
449+
'pod_name': 'pod-xyz-123',
450+
'project_id': 'my-project-id',
451+
'location': 'zone1'
452+
});
453+
});
454+
455+
it('should return a gce MonitoredResource', async () => {
456+
process.env.OC_RESOURCE_TYPE = 'cloud.google.com/gce/instance';
457+
process.env.OC_RESOURCE_LABELS = 'cloud.google.com/gce/instance_id=id1,' +
458+
'cloud.google.com/gce/zone=zone1';
459+
CoreResource.setup();
460+
const monitoredResource = await getDefaultResource('my-project-id');
461+
const {type, labels} = monitoredResource;
462+
463+
assert.equal(type, 'gce_instance');
464+
assert.equal(Object.keys(labels).length, 3);
465+
assert.deepStrictEqual(labels, {
466+
'instance_id': 'id1',
467+
'project_id': 'my-project-id',
468+
'zone': 'zone1'
469+
});
470+
});
471+
472+
it('should return a aws MonitoredResource', async () => {
473+
process.env.OC_RESOURCE_TYPE = 'aws.com/ec2/instance';
474+
process.env.OC_RESOURCE_LABELS = 'aws.com/ec2/account_id=id1,' +
475+
'aws.com/ec2/instance_id=instance1,aws.com/ec2/region=region1';
476+
CoreResource.setup();
477+
const monitoredResource = await getDefaultResource('my-project-id');
478+
const {type, labels} = monitoredResource;
479+
480+
assert.equal(type, 'aws_ec2_instance');
481+
assert.equal(Object.keys(labels).length, 4);
482+
assert.deepStrictEqual(labels, {
483+
'instance_id': 'instance1',
484+
'region': 'aws:region1',
485+
'project_id': 'my-project-id',
486+
'aws_account': 'id1'
487+
});
488+
});
489+
});
416490
});

packages/opencensus-resource-util/src/resource-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ async function getProjectId() {
158158
/** Gets instance id from GCP instance metadata. */
159159
async function getInstanceId() {
160160
try {
161-
return await gcpMetadata.instance('id');
161+
const id = await gcpMetadata.instance('id');
162+
return id.toString();
162163
} catch {
163164
return '';
164165
}

packages/opencensus-resource-util/test/test-detect-resource.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,16 @@ describe('detectResource', () => {
154154
.get(ZONE_PATH)
155155
.reply(200, () => 'project/zone/my-zone', HEADERS)
156156
.get(INSTANCE_ID_PATH)
157-
.reply(200, () => 'my-instance', HEADERS);
157+
.reply(200, () => 4520031799277581759, HEADERS);
158158
const {type, labels} = await resource.detectResource();
159159
scope.done();
160160

161161
assert.deepEqual(type, resource.GCP_GCE_INSTANCE_TYPE);
162162
assert.equal(Object.keys(labels).length, 3);
163163
assert.strictEqual(labels[resource.GCP_ACCOUNT_ID_KEY], 'my-project-id');
164164
assert.strictEqual(labels[resource.GCP_ZONE_KEY], 'my-zone');
165-
assert.strictEqual(labels[resource.GCP_INSTANCE_ID_KEY], 'my-instance');
165+
assert.strictEqual(
166+
labels[resource.GCP_INSTANCE_ID_KEY], '4520031799277582000');
166167
});
167168

168169
it('should retry if the initial request fails', async () => {
@@ -177,15 +178,16 @@ describe('detectResource', () => {
177178
.get(ZONE_PATH)
178179
.reply(200, () => 'project/zone/my-zone', HEADERS)
179180
.get(INSTANCE_ID_PATH)
180-
.reply(200, () => 'my-instance', HEADERS);
181+
.reply(200, () => 4520031799277581759, HEADERS);
181182
const {type, labels} = await resource.detectResource();
182183
scope.done();
183184

184185
assert.deepEqual(type, resource.GCP_GCE_INSTANCE_TYPE);
185186
assert.equal(Object.keys(labels).length, 3);
186187
assert.strictEqual(labels[resource.GCP_ACCOUNT_ID_KEY], 'my-project-id');
187188
assert.strictEqual(labels[resource.GCP_ZONE_KEY], 'my-zone');
188-
assert.strictEqual(labels[resource.GCP_INSTANCE_ID_KEY], 'my-instance');
189+
assert.strictEqual(
190+
labels[resource.GCP_INSTANCE_ID_KEY], '4520031799277582000');
189191
});
190192

191193
it('should return GCP_GCE_INSTANCE resource and empty data for non avaiable metadata attribute',
@@ -229,15 +231,16 @@ describe('detectResource', () => {
229231
.get(ZONE_PATH)
230232
.reply(200, () => 'project/zone/my-zone', HEADERS)
231233
.get(INSTANCE_ID_PATH)
232-
.reply(200, () => 'my-instance', HEADERS);
234+
.reply(200, () => 4520031799277581759, HEADERS);
233235
const {type, labels} = await resource.detectResource();
234236
scope.done();
235237

236238
assert.deepEqual(type, 'global');
237239
assert.equal(Object.keys(labels).length, 5);
238240
assert.strictEqual(labels[resource.GCP_ACCOUNT_ID_KEY], 'my-project-id');
239241
assert.strictEqual(labels[resource.GCP_ZONE_KEY], 'zone1');
240-
assert.strictEqual(labels[resource.GCP_INSTANCE_ID_KEY], 'my-instance');
242+
assert.strictEqual(
243+
labels[resource.GCP_INSTANCE_ID_KEY], '4520031799277582000');
241244
assert.strictEqual(labels['user'], 'user1');
242245
assert.strictEqual(labels['version'], '1.0');
243246
});

0 commit comments

Comments
 (0)