Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(monitoring-exporter): convert to GCM format based on metric data type, not instrument type #453

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -811,3 +811,95 @@ exports['MetricExporter snapshot tests UpDownCounter - INT 1'] = [
]
}
]

exports['MetricExporter snapshot tests reconfigure with views counter with histogram view 1'] = [
{
"uri": "/v3/projects/otel-starter-project/metricDescriptors",
"body": {
"type": "workload.googleapis.com/myrenamedhistogram",
"description": "instrument description",
"displayName": "myrenamedhistogram",
"metricKind": "CUMULATIVE",
"valueType": "DOUBLE",
"unit": "{myunit}",
"labels": [
{
"key": "opentelemetry_task",
"description": "OpenTelemetry task identifier"
}
]
},
"userAgent": [
"opentelemetry-js/1.8.0 google-cloud-metric-exporter/0.14.0 google-api-nodejs-client/5.1.0 (gzip)"
]
},
{
"uri": "/v3/projects/otel-starter-project/timeSeries",
"body": {
"timeSeries": [
{
"metric": {
"type": "workload.googleapis.com/myrenamedhistogram",
"labels": {
"opentelemetry_task": "opentelemetry_task"
}
},
"resource": {
"type": "global",
"labels": {
"project_id": "otel-starter-project"
}
},
"metricKind": "CUMULATIVE",
"valueType": "DOUBLE",
"points": [
{
"value": {
"distributionValue": {
"count": "1",
"mean": 12.3,
"bucketOptions": {
"explicitBuckets": {
"bounds": [
0,
5,
10,
25,
50,
75,
100,
250,
500,
1000
]
}
},
"bucketCounts": [
"0",
"0",
"0",
"1",
"0",
"0",
"0",
"0",
"0",
"0",
"0"
]
}
},
"interval": {
"startTime": "startTime",
"endTime": "endTime"
}
}
]
}
]
},
"userAgent": [
"opentelemetry-js/1.8.0 google-cloud-metric-exporter/0.14.0 google-api-nodejs-client/5.1.0 (gzip)"
]
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import {
PushMetricExporter,
ResourceMetrics,
InstrumentDescriptor,
MetricData,
} from '@opentelemetry/sdk-metrics';
import {
ExportResult,
Expand Down Expand Up @@ -62,12 +62,14 @@ export class MetricExporter implements PushMetricExporter {
private _projectId: string | void | Promise<string | void>;
private readonly _metricPrefix: string;
private readonly _auth: GoogleAuth;
private readonly _startTime = new Date().toISOString();

static readonly DEFAULT_METRIC_PREFIX: string = 'workload.googleapis.com';

private registeredInstrumentDescriptors: Map<string, InstrumentDescriptor> =
new Map();
/**
* Set of OTel metric names that have already had their metric descriptors successfully
* created
*/
private createdMetricDescriptors: Set<string> = new Set();

private _monitoring: monitoring_v3.Monitoring;

Expand Down Expand Up @@ -144,9 +146,7 @@ export class MetricExporter implements PushMetricExporter {
const timeSeries: TimeSeries[] = [];
for (const scopeMetric of resourceMetrics.scopeMetrics) {
for (const metric of scopeMetric.metrics) {
const isRegistered = await this._registerMetricDescriptor(
metric.descriptor
);
const isRegistered = await this._registerMetricDescriptor(metric);
if (isRegistered) {
timeSeries.push(
...createTimeSeries(metric, resource, this._metricPrefix)
Expand Down Expand Up @@ -180,64 +180,50 @@ export class MetricExporter implements PushMetricExporter {
/**
* Returns true if the given metricDescriptor is successfully registered to
* Google Cloud Monitoring, or the exact same metric has already been
* registered. Returns false otherwise.
* @param instrumentDescriptor The OpenTelemetry MetricDescriptor.
* registered. Returns false otherwise and should be skipped.
*
* @param metric The OpenTelemetry MetricData.
*/
private async _registerMetricDescriptor(
instrumentDescriptor: InstrumentDescriptor
) {
const existingInstrumentDescriptor =
this.registeredInstrumentDescriptors.get(instrumentDescriptor.name);
metric: MetricData
): Promise<boolean> {
const isDescriptorCreated = this.createdMetricDescriptors.has(
metric.descriptor.name
);

if (existingInstrumentDescriptor) {
if (existingInstrumentDescriptor === instrumentDescriptor) {
// Ignore descriptors that are already registered.
return true;
} else {
diag.warn(
'A different metric with the same name is already registered: %s',
existingInstrumentDescriptor
);
return false;
}
if (isDescriptorCreated) {
return true;
}

try {
await this._createMetricDescriptor(instrumentDescriptor);
this.registeredInstrumentDescriptors.set(
instrumentDescriptor.name,
instrumentDescriptor
);
const res = await this._createMetricDescriptor(metric);
if (res) {
this.createdMetricDescriptors.add(metric.descriptor.name);
return true;
} catch (e) {
const err = asError(e);
diag.error('Error creating metric descriptor: %s', err.message);
return false;
}
return false;
}

/**
* Calls CreateMetricDescriptor in the GCM API for the given InstrumentDescriptor
* @param instrumentDescriptor The OpenTelemetry InstrumentDescriptor.
* @param metric The OpenTelemetry MetricData.
* @returns whether or not the descriptor was successfully created
*/
private async _createMetricDescriptor(
instrumentDescriptor: InstrumentDescriptor
) {
private async _createMetricDescriptor(metric: MetricData): Promise<boolean> {
const authClient = await this._authorize();
const descriptor = transformMetricDescriptor(
instrumentDescriptor,
this._metricPrefix
);
const descriptor = transformMetricDescriptor(metric, this._metricPrefix);

try {
await this._monitoring.projects.metricDescriptors.create({
name: `projects/${this._projectId}`,
requestBody: descriptor,
auth: authClient,
});
diag.debug('sent metric descriptor', descriptor);
return true;
} catch (e) {
const err = asError(e);
diag.error('Failed to create metric descriptor: %s', err.message);
return false;
}
}

Expand Down
62 changes: 38 additions & 24 deletions packages/opentelemetry-cloud-monitoring-exporter/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import {
InstrumentDescriptor,
InstrumentType,
Histogram,
MetricData,
DataPoint,
Expand All @@ -39,17 +38,28 @@ const OPENTELEMETRY_TASK = 'opentelemetry_task';
const OPENTELEMETRY_TASK_DESCRIPTION = 'OpenTelemetry task identifier';
export const OPENTELEMETRY_TASK_VALUE_DEFAULT = generateDefaultTaskValue();

/**
*
* @param metric the MetricData to create a descriptor for
* @param metricPrefix prefix to add to metric names
* @param displayNamePrefix prefix to add to display name in the descriptor
* @returns the GCM MetricDescriptor or null if the MetricData was empty
*/
export function transformMetricDescriptor(
instrumentDescriptor: InstrumentDescriptor,
metric: MetricData,
metricPrefix: string
): MetricDescriptor {
const {
descriptor: {name, description, unit},
} = metric;

return {
type: transformMetricType(metricPrefix, instrumentDescriptor.name),
description: instrumentDescriptor.description,
displayName: instrumentDescriptor.name,
metricKind: transformMetricKind(instrumentDescriptor.type),
valueType: transformValueType(instrumentDescriptor.valueType),
unit: instrumentDescriptor.unit,
type: transformMetricType(metricPrefix, name),
description,
displayName: name,
metricKind: transformMetricKind(metric),
valueType: transformValueType(metric),
unit,
labels: [
{
key: OPENTELEMETRY_TASK,
Expand All @@ -65,30 +75,34 @@ function transformMetricType(metricPrefix: string, name: string): string {
}

/** Transforms a OpenTelemetry instrument type to a GCM MetricKind. */
function transformMetricKind(instrumentType: InstrumentType): MetricKind {
switch (instrumentType) {
case InstrumentType.COUNTER:
case InstrumentType.OBSERVABLE_COUNTER:
case InstrumentType.HISTOGRAM:
return MetricKind.CUMULATIVE;
case InstrumentType.UP_DOWN_COUNTER:
case InstrumentType.OBSERVABLE_GAUGE:
case InstrumentType.OBSERVABLE_UP_DOWN_COUNTER:
function transformMetricKind(metric: MetricData): MetricKind {
switch (metric.dataPointType) {
case DataPointType.SUM:
return metric.isMonotonic ? MetricKind.CUMULATIVE : MetricKind.GAUGE;
case DataPointType.GAUGE:
return MetricKind.GAUGE;
case DataPointType.HISTOGRAM:
return MetricKind.CUMULATIVE;
default:
exhaust(instrumentType);
diag.info('Encountered unexpected instrumentType=%s', instrumentType);
exhaust(metric);
diag.info(
'Encountered unexpected data point type %s',
(metric as MetricData).dataPointType
);
return MetricKind.UNSPECIFIED;
}
}

/** Transforms a OpenTelemetry ValueType to a GCM ValueType. */
function transformValueType(valueType: OTValueType): ValueType {
function transformValueType(metric: MetricData): ValueType {
const {valueType} = metric.descriptor;
if (valueType === OTValueType.DOUBLE) {
return ValueType.DOUBLE;
} else if (valueType === OTValueType.INT) {
return ValueType.INT64;
} else {
exhaust(valueType);
diag.info('Encountered unexpected value type %s', valueType);
return ValueType.VALUE_TYPE_UNSPECIFIED;
}
}
Expand All @@ -97,13 +111,13 @@ function transformValueType(valueType: OTValueType): ValueType {
* Converts metric's timeseries to a TimeSeries, so that metric can be
* uploaded to GCM.
*/
export function createTimeSeries<TMetricData extends MetricData>(
metric: TMetricData,
export function createTimeSeries(
metric: MetricData,
resource: MonitoredResource,
metricPrefix: string
): TimeSeries[] {
const metricKind = transformMetricKind(metric.descriptor.type);
const valueType = transformValueType(metric.descriptor.valueType);
const metricKind = transformMetricKind(metric);
const valueType = transformValueType(metric);

return transformPoints(metric, metricPrefix).map(({point, metric}) => ({
metric,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import {Attributes, ValueType} from '@opentelemetry/api';
import * as snapshot from 'snap-shot-it';
import {ExportResult, ExportResultCode} from '@opentelemetry/core';
import {ResourceMetrics} from '@opentelemetry/sdk-metrics';
import {Aggregation, ResourceMetrics, View} from '@opentelemetry/sdk-metrics';
import * as assert from 'assert';
import * as nock from 'nock';
import * as sinon from 'sinon';
Expand Down Expand Up @@ -230,6 +230,35 @@ describe('MetricExporter snapshot tests', () => {
gcmNock.snapshotCalls();
});
});

describe('reconfigure with views', () => {
it('counter with histogram view', async () => {
const resourceMetrics = await generateMetricsData(
(_, meter) => {
meter
.createCounter('mycounter', {
description: 'instrument description',
unit: '{myunit}',
valueType: ValueType.DOUBLE,
})
.add(12.3);
},
{
views: [
new View({
instrumentName: 'mycounter',
aggregation: Aggregation.Histogram(),
name: 'myrenamedhistogram',
}),
],
}
);

const result = await callExporter(resourceMetrics);
assert.deepStrictEqual(result, {code: ExportResultCode.SUCCESS});
gcmNock.snapshotCalls();
});
});
});

function callExporter(resourceMetrics: ResourceMetrics): Promise<ExportResult> {
Expand Down
Loading