UNPKG

@google-cloud/spanner

Version:
272 lines 11.1 kB
"use strict"; // Copyright 2025 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 // // http://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. Object.defineProperty(exports, "__esModule", { value: true }); exports._TEST_ONLY = void 0; exports.transformResourceMetricToTimeSeriesArray = transformResourceMetricToTimeSeriesArray; const sdk_metrics_1 = require("@opentelemetry/sdk-metrics"); const path = require("path"); const external_types_1 = require("./external-types"); const constants_1 = require("./constants"); const metrics_tracer_factory_1 = require("./metrics-tracer-factory"); /** Transforms a OpenTelemetry instrument type to a GCM MetricKind. */ function _transformMetricKind(metric) { switch (metric.dataPointType) { case sdk_metrics_1.DataPointType.SUM: return metric.isMonotonic ? external_types_1.MetricKind.CUMULATIVE : external_types_1.MetricKind.GAUGE; case sdk_metrics_1.DataPointType.GAUGE: return external_types_1.MetricKind.GAUGE; case sdk_metrics_1.DataPointType.HISTOGRAM: case sdk_metrics_1.DataPointType.EXPONENTIAL_HISTOGRAM: return external_types_1.MetricKind.CUMULATIVE; default: exhaust(metric); // No logging needed as it will be done in transformPoints() return external_types_1.MetricKind.UNSPECIFIED; } } /** Transforms resource to Google Cloud Monitoring monitored resource */ function _transformResource(labels) { return { type: constants_1.SPANNER_RESOURCE_TYPE, labels: labels, }; } /** Transforms a OpenTelemetry ValueType to a GCM ValueType. */ function _transformValueType(metric) { const { dataPointType, descriptor: { name }, } = metric; if (dataPointType === sdk_metrics_1.DataPointType.HISTOGRAM || dataPointType === sdk_metrics_1.DataPointType.EXPONENTIAL_HISTOGRAM) { return external_types_1.ValueType.DISTRIBUTION; } else if (dataPointType === sdk_metrics_1.DataPointType.SUM) { return external_types_1.ValueType.INT64; } else if (dataPointType === sdk_metrics_1.DataPointType.GAUGE) { return external_types_1.ValueType.DOUBLE; } console.warn('Encountered unexpected metric %s', name); return external_types_1.ValueType.VALUE_TYPE_UNSPECIFIED; } /** * Convert the metrics data to a list of Google Cloud Monitoring time series. */ function transformResourceMetricToTimeSeriesArray(resourceMetrics) { const resource = resourceMetrics?.resource; const scopeMetrics = resourceMetrics?.scopeMetrics; if (!scopeMetrics) return []; return (scopeMetrics // Only keep those whose scope.name matches 'spanner-nodejs'. .filter(({ scope: { name } }) => name === constants_1.SPANNER_METER_NAME) // Takes each metric array and flattens it into one array .flatMap(({ metrics }) => // Only keeps metrics that match our spanner metric names metrics.filter(metric => constants_1.METRIC_NAMES.has(metric.descriptor.name))) // Flatmap the data points in each metric to create a TimeSeries for each point .flatMap(metric => metric.dataPoints.flatMap(dataPoint => _createTimeSeries(metric, dataPoint, resource)))); } /** * Creates a GCM TimeSeries. */ function _createTimeSeries(metric, dataPoint, resource) { const type = path.posix.join(constants_1.CLIENT_METRICS_PREFIX, metric.descriptor.name); const resourceLabels = resource ? _extractLabels(resource) : { metricLabels: {}, monitoredResourceLabels: {} }; const dataLabels = _extractLabels(dataPoint); const labels = { ...resourceLabels.metricLabels, ...dataLabels.metricLabels, }; const monitoredResourceLabels = { ...resourceLabels.monitoredResourceLabels, ...dataLabels.monitoredResourceLabels, }; const transformedMetric = { type, labels, }; return { metric: transformedMetric, resource: _transformResource(monitoredResourceLabels), metricKind: _transformMetricKind(metric), valueType: _transformValueType(metric), points: [_transformPoint(metric, dataPoint)], unit: metric.descriptor.unit, }; } /** * Transform timeseries's point, so that metric can be uploaded to GCM. */ function _transformPoint(metric, dataPoint) { switch (metric.dataPointType) { case sdk_metrics_1.DataPointType.SUM: case sdk_metrics_1.DataPointType.GAUGE: return { value: _transformNumberValue(_transformValueType(metric), dataPoint.value), interval: { // Add start time for non-gauge points ...(metric.dataPointType === sdk_metrics_1.DataPointType.SUM && metric.isMonotonic ? { startTime: _formatHrTimeToGcmTime(dataPoint.startTime), } : null), endTime: _formatHrTimeToGcmTime(dataPoint.endTime), }, }; case sdk_metrics_1.DataPointType.HISTOGRAM: return { value: _transformHistogramValue(dataPoint.value), interval: { startTime: _formatHrTimeToGcmTime(dataPoint.startTime), endTime: _formatHrTimeToGcmTime(dataPoint.endTime), }, }; case sdk_metrics_1.DataPointType.EXPONENTIAL_HISTOGRAM: return { value: _transformExponentialHistogramValue(dataPoint.value), interval: { startTime: _formatHrTimeToGcmTime(dataPoint.startTime), endTime: _formatHrTimeToGcmTime(dataPoint.endTime), }, }; default: exhaust(metric); return { value: dataPoint.value, interval: { endTime: _formatHrTimeToGcmTime(dataPoint.endTime), }, }; } } /** Extracts metric and monitored resource labels from data point */ function _extractLabels({ attributes = {} }) { const factory = metrics_tracer_factory_1.MetricsTracerFactory.getInstance(); // Add Client name and Client UID metric labels attributes[constants_1.METRIC_LABEL_KEY_CLIENT_UID] = factory?.clientUid ?? constants_1.UNKNOWN_ATTRIBUTE; attributes[constants_1.METRIC_LABEL_KEY_CLIENT_NAME] = factory?.clientName ?? constants_1.UNKNOWN_ATTRIBUTE; return Object.entries(attributes).reduce((result, [key, value]) => { const normalizedKey = _normalizeLabelKey(key); const val = value?.toString(); if (constants_1.METRIC_LABELS.has(key)) result.metricLabels[normalizedKey] = val; if (constants_1.MONITORED_RESOURCE_LABELS.has(key)) result.monitoredResourceLabels[normalizedKey] = val; return result; }, { metricLabels: {}, monitoredResourceLabels: {} }); } function _normalizeLabelKey(key) { // Replace characters which are not Letter or Decimal_Number unicode category with "_", see // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes // // Reimplementation of reference impl in Go: // https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/e955c204f4f2bfdc92ff0ad52786232b975efcc2/exporter/metric/metric.go#L595-L604 let sanitized = key.replace(/[^\p{Letter}\p{Decimal_Number}_]/gu, '_'); if (sanitized[0].match(/\p{Decimal_Number}/u)) { sanitized = 'key_' + sanitized; } return sanitized; } /** Transforms a OpenTelemetry Point's value to a GCM Point value. */ function _transformNumberValue(valueType, value) { if (valueType === external_types_1.ValueType.INT64) { return { int64Value: Math.round(value).toString() }; } else if (valueType === external_types_1.ValueType.DOUBLE) { const doubleString = Number.isInteger(value) ? `${value}.0` : value.toString(); return { doubleValue: doubleString }; } throw Error(`unsupported value type: ${valueType}`); } function _transformHistogramValue(value) { return { distributionValue: { // sumOfSquaredDeviation param not aggregated count: value.count.toString(), mean: value.count && value.sum ? value.sum / value.count : 0, bucketOptions: { explicitBuckets: { bounds: value.buckets.boundaries }, }, bucketCounts: value.buckets.counts.map(value => value.toString()), }, }; } function _transformExponentialHistogramValue(value) { // Adapated from reference impl in Go which has more explanatory comments // https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/exporter/collector/metrics.go#L582 const underflow = value.zeroCount + value.negative.bucketCounts.reduce((prev, current) => prev + current, 0); const bucketCounts = [ underflow, ...value.positive.bucketCounts, 0, // overflow bucket is always empty ]; let bucketOptions; if (value.positive.bucketCounts.length === 0) { bucketOptions = { explicitBuckets: { bounds: [] }, }; } else { const growthFactor = Math.pow(2, Math.pow(2, -value.scale)); //exp2(exp2(-value.scale)); const scale = Math.pow(growthFactor, value.positive.offset); bucketOptions = { exponentialBuckets: { growthFactor, scale, numFiniteBuckets: bucketCounts.length - 2, }, }; } const mean = value.sum === undefined || value.count === 0 ? 0 : value.sum / value.count; return { distributionValue: { // sumOfSquaredDeviation param not aggregated count: value.count.toString(), mean, bucketOptions, bucketCounts: bucketCounts.map(value => value.toString()), }, }; } /** Transforms an OpenTelemetry time value to a GCM time value. */ function _formatHrTimeToGcmTime(hrTime) { return { seconds: hrTime[0], nanos: hrTime[1], }; } /** * Assert switch case is exhaustive */ function exhaust(switchValue) { return switchValue; } exports._TEST_ONLY = { _normalizeLabelKey, _transformMetricKind, _extractLabels, _formatHrTimeToGcmTime, _transformResource, _transformPoint, _transformValueType, transformResourceMetricToTimeSeriesArray, }; //# sourceMappingURL=transform.js.map