@google-cloud/spanner
Version:
Cloud Spanner Client Library for Node.js
272 lines • 11.1 kB
JavaScript
;
// 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