UNPKG

@google-cloud/bigtable

Version:
317 lines 14.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 // // https://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.OperationMetricsCollector = void 0; const fs = require("fs"); const gax = require("google-gax"); const path = require("path"); // When this environment variable is set then print any errors associated // with failures in the metrics collector. const METRICS_DEBUG = process.env.METRICS_DEBUG; const protoPath = path.join(__dirname, '../../protos/google/bigtable/v2/response_params.proto'); const root = gax.protobuf.loadSync(protoPath); const ResponseParams = root.lookupType('ResponseParams'); const { hrtime } = require('node:process'); const packageJSON = fs.readFileSync('package.json'); const version = JSON.parse(packageJSON.toString()).version; // MetricsCollectorState is a list of states that the metrics collector can be in. // Tracking the OperationMetricsCollector state is done so that the // OperationMetricsCollector methods are not called in the wrong order. If the // methods are called in the wrong order they will not execute and they will // throw warnings. // // The following state transitions are allowed: // OPERATION_NOT_STARTED -> OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS // OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS -> OPERATION_STARTED_ATTEMPT_IN_PROGRESS // OPERATION_STARTED_ATTEMPT_IN_PROGRESS -> OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS // OPERATION_STARTED_ATTEMPT_IN_PROGRESS -> OPERATION_COMPLETE var MetricsCollectorState; (function (MetricsCollectorState) { MetricsCollectorState[MetricsCollectorState["OPERATION_NOT_STARTED"] = 0] = "OPERATION_NOT_STARTED"; MetricsCollectorState[MetricsCollectorState["OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS"] = 1] = "OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS"; MetricsCollectorState[MetricsCollectorState["OPERATION_STARTED_ATTEMPT_IN_PROGRESS_NO_ROWS_YET"] = 2] = "OPERATION_STARTED_ATTEMPT_IN_PROGRESS_NO_ROWS_YET"; MetricsCollectorState[MetricsCollectorState["OPERATION_STARTED_ATTEMPT_IN_PROGRESS_SOME_ROWS_RECEIVED"] = 3] = "OPERATION_STARTED_ATTEMPT_IN_PROGRESS_SOME_ROWS_RECEIVED"; MetricsCollectorState[MetricsCollectorState["OPERATION_COMPLETE"] = 4] = "OPERATION_COMPLETE"; })(MetricsCollectorState || (MetricsCollectorState = {})); // This method displays warnings if METRICS_DEBUG is enabled. function withMetricsDebug(fn) { try { return fn(); } catch (e) { if (METRICS_DEBUG) { console.warn('METRICS_DEBUG warning'); console.warn(e.message); } } return; } // Checks that the state transition is valid and if not it throws a warning. function checkState(currentState, allowedStates) { if (allowedStates.includes(currentState)) { return; } else { throw Error('Invalid state transition'); } } /** * A class for tracing and recording client-side metrics related to Bigtable operations. */ class OperationMetricsCollector { // The following key corresponds to the key the instance information is // stored in for the metadata that gets returned from the server. INSTANCE_INFORMATION_KEY = 'x-goog-ext-425905942-bin'; state; operationStartTime; attemptStartTime; zone; cluster; tabularApiSurface; methodName; attemptCount = 0; firstResponseLatency; serverTimeRead; serverTime; connectivityErrorCount; streamingOperation; handlers; userStream; /** * @param {ITabularApiSurface} tabularApiSurface Information about the Bigtable table being accessed. * @param {MethodName} methodName The name of the method being traced. * @param {StreamingState} streamingOperation Whether or not the call is a streaming operation. * @param {IMetricsHandler[]} handlers The metrics handlers used to store the record the metrics. */ constructor(tabularApiSurface, methodName, streamingOperation, handlers) { this.state = MetricsCollectorState.OPERATION_NOT_STARTED; this.zone = undefined; this.cluster = undefined; this.tabularApiSurface = tabularApiSurface; this.methodName = methodName; this.operationStartTime = null; this.attemptStartTime = null; this.firstResponseLatency = null; this.serverTimeRead = false; this.serverTime = null; this.connectivityErrorCount = 0; this.streamingOperation = streamingOperation; this.handlers = handlers; } getMetricsCollectorData() { const appProfileId = this.tabularApiSurface.bigtable.appProfileId; return Object.assign({ instanceId: this.tabularApiSurface.instance.id, table: this.tabularApiSurface.id, cluster: this.cluster || '<unspecified>', zone: this.zone || 'global', method: this.methodName, }, appProfileId ? { app_profile: appProfileId } : {}); } /** * Called to add handlers to the stream so that we can observe * header and trailer data for client side metrics. * * @param stream */ wrapRequest(stream) { stream .on('metadata', (metadata) => { this.onMetadataReceived(metadata); }) .on('status', (status) => { this.onStatusMetadataReceived(status); }); } /** * Called when the operation starts. Records the start time. */ onOperationStart() { withMetricsDebug(() => { checkState(this.state, [MetricsCollectorState.OPERATION_NOT_STARTED]); this.operationStartTime = hrtime.bigint(); this.firstResponseLatency = null; this.state = MetricsCollectorState.OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS; }); } /** * Called when an attempt (e.g., an RPC attempt) completes. Records attempt latencies. * @param {grpc.status} attemptStatus The grpc status for the attempt. */ onAttemptComplete(attemptStatus) { withMetricsDebug(() => { checkState(this.state, [ MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_NO_ROWS_YET, MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_SOME_ROWS_RECEIVED, ]); this.state = MetricsCollectorState.OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS; this.attemptCount++; const endTime = hrtime.bigint(); if (this.attemptStartTime) { const totalMilliseconds = Number((endTime - this.attemptStartTime) / BigInt(1000000)); this.handlers.forEach(metricsHandler => { if (metricsHandler.onAttemptComplete) { metricsHandler.onAttemptComplete({ attemptLatency: totalMilliseconds, serverLatency: this.serverTime ?? undefined, connectivityErrorCount: this.connectivityErrorCount, streaming: this.streamingOperation, status: attemptStatus.toString(), client_name: `nodejs-bigtable/${version}`, metricsCollectorData: this.getMetricsCollectorData(), }); } }); } else { console.warn('Start time should always be provided'); } }); } /** * Called when a new attempt starts. Records the start time of the attempt. */ onAttemptStart() { withMetricsDebug(() => { checkState(this.state, [ MetricsCollectorState.OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS, ]); this.state = MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_NO_ROWS_YET; this.attemptStartTime = hrtime.bigint(); this.serverTime = null; this.serverTimeRead = false; this.connectivityErrorCount = 0; }); } /** * Called when the first response is received. Records first response latencies. */ onResponse() { withMetricsDebug(() => { if (!this.firstResponseLatency) { checkState(this.state, [ MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_NO_ROWS_YET, ]); this.state = MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_SOME_ROWS_RECEIVED; const endTime = hrtime.bigint(); if (this.operationStartTime) { this.firstResponseLatency = Number((endTime - this.operationStartTime) / BigInt(1000000)); } else { console.warn('ProjectId and operationStartTime should always be provided'); } } }); } /** * Called when an operation completes (successfully or unsuccessfully). * Records operation latencies, retry counts, and connectivity error counts. * @param {grpc.status} finalOperationStatus Information about the completed operation. * @param {number} applicationLatency The application latency measurement. */ onOperationComplete(finalOperationStatus, applicationLatency) { withMetricsDebug(() => { if (this.state === MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_NO_ROWS_YET || this.state === MetricsCollectorState.OPERATION_STARTED_ATTEMPT_IN_PROGRESS_SOME_ROWS_RECEIVED) { this.onAttemptComplete(finalOperationStatus); } checkState(this.state, [ MetricsCollectorState.OPERATION_STARTED_ATTEMPT_NOT_IN_PROGRESS, ]); this.state = MetricsCollectorState.OPERATION_COMPLETE; const endTime = hrtime.bigint(); if (this.operationStartTime) { const totalMilliseconds = Number((endTime - this.operationStartTime) / BigInt(1000000)); { this.handlers.forEach(metricsHandler => { if (metricsHandler.onOperationComplete) { metricsHandler.onOperationComplete({ status: finalOperationStatus.toString(), streaming: this.streamingOperation, metricsCollectorData: this.getMetricsCollectorData(), client_name: `nodejs-bigtable/${version}`, operationLatency: totalMilliseconds, retryCount: this.attemptCount - 1, firstResponseLatency: this.firstResponseLatency ?? 0, applicationLatency: applicationLatency ?? 0, }); } }); } } else { console.warn('operation start time should always be available here'); } }); } /** * Called when metadata is received. Extracts server timing information if available. * @param {object} metadata The received metadata. */ onMetadataReceived(metadata) { if (!this.serverTimeRead && this.connectivityErrorCount < 1) { // Check serverTimeRead, connectivityErrorCount here to reduce latency. const mappedEntries = new Map(Array.from(metadata.internalRepr.entries(), ([key, value]) => [ key, value.toString(), ])); const SERVER_TIMING_REGEX = /.*gfet4t7;\s*dur=(\d+\.?\d*).*/; const SERVER_TIMING_KEY = 'server-timing'; const durationValues = mappedEntries.get(SERVER_TIMING_KEY); const matchedDuration = durationValues?.match(SERVER_TIMING_REGEX); if (matchedDuration && matchedDuration[1]) { if (!this.serverTimeRead) { this.serverTimeRead = true; this.serverTime = isNaN(parseInt(matchedDuration[1])) ? null : parseInt(matchedDuration[1]); } } else { this.connectivityErrorCount = 1; } } } /** * Called when status information is received. Extracts zone and cluster information. * @param {object} status The received status information. */ onStatusMetadataReceived(status) { withMetricsDebug(() => { if (!this.zone || !this.cluster) { const mappedValue = status.metadata.internalRepr.get(this.INSTANCE_INFORMATION_KEY); if (mappedValue && mappedValue[0] && ResponseParams) { const decodedValue = ResponseParams.decode(mappedValue[0], mappedValue[0].length); if (decodedValue && decodedValue.zoneId) { this.zone = decodedValue.zoneId; } if (decodedValue && decodedValue.clusterId) { this.cluster = decodedValue.clusterId; } } } }); } } exports.OperationMetricsCollector = OperationMetricsCollector; //# sourceMappingURL=operation-metrics-collector.js.map