diff --git a/packages/opentelemetry-cloud-monitoring-exporter/__snapshots__/instrument-snapshot.test.ts.js b/packages/opentelemetry-cloud-monitoring-exporter/__snapshots__/instrument-snapshot.test.ts.js index 119e1941b..57492baa7 100644 --- a/packages/opentelemetry-cloud-monitoring-exporter/__snapshots__/instrument-snapshot.test.ts.js +++ b/packages/opentelemetry-cloud-monitoring-exporter/__snapshots__/instrument-snapshot.test.ts.js @@ -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": "custom.googleapis.com/opentelemetry/myrenamedhistogram", + "description": "instrument description", + "displayName": "OpenTelemetry/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": "custom.googleapis.com/opentelemetry/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)" + ] + } +] diff --git a/packages/opentelemetry-cloud-monitoring-exporter/src/monitoring.ts b/packages/opentelemetry-cloud-monitoring-exporter/src/monitoring.ts index 3b8d81c9d..ce5050d11 100644 --- a/packages/opentelemetry-cloud-monitoring-exporter/src/monitoring.ts +++ b/packages/opentelemetry-cloud-monitoring-exporter/src/monitoring.ts @@ -15,7 +15,7 @@ import { PushMetricExporter, ResourceMetrics, - InstrumentDescriptor, + MetricData, } from '@opentelemetry/sdk-metrics'; import { ExportResult, @@ -63,14 +63,16 @@ export class MetricExporter implements PushMetricExporter { private readonly _metricPrefix: string; private readonly _displayNamePrefix: string; private readonly _auth: GoogleAuth; - private readonly _startTime = new Date().toISOString(); static readonly DEFAULT_DISPLAY_NAME_PREFIX: string = 'OpenTelemetry'; static readonly CUSTOM_OPENTELEMETRY_DOMAIN: string = 'custom.googleapis.com/opentelemetry'; - private registeredInstrumentDescriptors: Map = - new Map(); + /** + * Set of OTel metric names that have already had their metric descriptors successfully + * created + */ + private createdMetricDescriptors: Set = new Set(); private _monitoring: monitoring_v3.Monitoring; @@ -150,9 +152,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) @@ -186,65 +186,61 @@ 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 { + 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 { const authClient = await this._authorize(); const descriptor = transformMetricDescriptor( - instrumentDescriptor, + metric, this._metricPrefix, this._displayNamePrefix ); + if (!descriptor) { + diag.info( + 'MetricData with name "%s" contained no points, skipping.', + metric.descriptor.name + ); + return false; + } + try { - await this._monitoring.projects.metricDescriptors.create({ + const res = 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; } } diff --git a/packages/opentelemetry-cloud-monitoring-exporter/src/transform.ts b/packages/opentelemetry-cloud-monitoring-exporter/src/transform.ts index b01c77224..82bbda42a 100644 --- a/packages/opentelemetry-cloud-monitoring-exporter/src/transform.ts +++ b/packages/opentelemetry-cloud-monitoring-exporter/src/transform.ts @@ -14,7 +14,6 @@ import { InstrumentDescriptor, - InstrumentType, Histogram, MetricData, DataPoint, @@ -39,21 +38,34 @@ 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, displayNamePrefix: string -): MetricDescriptor { +): MetricDescriptor | null { + // If no data points + if (!metric.dataPoints[0]) { + return null; + } + + const { + descriptor: {name, description, unit}, + } = metric; + return { - type: transformMetricType(metricPrefix, instrumentDescriptor.name), - description: instrumentDescriptor.description, - displayName: transformDisplayName( - displayNamePrefix, - instrumentDescriptor.name - ), - metricKind: transformMetricKind(instrumentDescriptor.type), - valueType: transformValueType(instrumentDescriptor.valueType), - unit: instrumentDescriptor.unit, + type: transformMetricType(metricPrefix, name), + description, + displayName: transformDisplayName(displayNamePrefix, name), + metricKind: transformMetricKind(metric), + valueType: transformValueType(metric), + unit, labels: [ { key: OPENTELEMETRY_TASK, @@ -74,30 +86,34 @@ function transformDisplayName(displayNamePrefix: 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; } } @@ -106,13 +122,13 @@ function transformValueType(valueType: OTValueType): ValueType { * Converts metric's timeseries to a TimeSeries, so that metric can be * uploaded to GCM. */ -export function createTimeSeries( - 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, diff --git a/packages/opentelemetry-cloud-monitoring-exporter/test/instrument-snapshot.test.ts b/packages/opentelemetry-cloud-monitoring-exporter/test/instrument-snapshot.test.ts index b890e83d3..59b50393b 100644 --- a/packages/opentelemetry-cloud-monitoring-exporter/test/instrument-snapshot.test.ts +++ b/packages/opentelemetry-cloud-monitoring-exporter/test/instrument-snapshot.test.ts @@ -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'; @@ -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 {