Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 4cbc399

Browse files
authored
Merge branch 'main' into remove-the-mocha-dependency
2 parents 692e006 + 71f4d78 commit 4cbc399

27 files changed

Lines changed: 2052 additions & 819 deletions

src/client-side-metrics/client-side-metrics-attributes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum StreamingState {
2525
* metrics, allowing for differentiation of performance by method.
2626
*/
2727
export enum MethodName {
28+
READ_ROW = 'Bigtable.ReadRow',
2829
READ_ROWS = 'Bigtable.ReadRows',
2930
MUTATE_ROW = 'Bigtable.MutateRow',
3031
CHECK_AND_MUTATE_ROW = 'Bigtable.CheckAndMutateRow',

src/client-side-metrics/exporter.ts

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ import {
1919
Histogram,
2020
ResourceMetrics,
2121
} from '@opentelemetry/sdk-metrics';
22-
import {grpc, ServiceError} from 'google-gax';
22+
import {ClientOptions, ServiceError} from 'google-gax';
2323
import {MetricServiceClient} from '@google-cloud/monitoring';
2424
import {google} from '@google-cloud/monitoring/build/protos/protos';
2525
import ICreateTimeSeriesRequest = google.monitoring.v3.ICreateTimeSeriesRequest;
26-
import {RetryOptions} from 'google-gax';
2726

2827
export interface ExportResult {
2928
code: number;
@@ -119,7 +118,7 @@ function getIntegerPoints(dataPoint: DataPoint<number>) {
119118
* getResource gets the resource object which is used for building the timeseries
120119
* object that will be sent to Google Cloud Monitoring dashboard
121120
*
122-
* @param {string} metricName The backend name of the metric that we want to record
121+
* @param {string} projectId The name of the project
123122
* @param {DataPoint} dataPoint The datapoint containing the data we wish to
124123
* send to the Google Cloud Monitoring dashboard
125124
*/
@@ -184,6 +183,7 @@ function getMetric(
184183
* metric attributes, data points, and aggregation information, into an object
185184
* that conforms to the expected request format of the Cloud Monitoring API.
186185
*
186+
* @param projectId
187187
* @param {ResourceMetrics} exportArgs - The OpenTelemetry metrics data to be converted. This
188188
* object contains resource attributes, scope information, and a list of
189189
* metrics with their associated data points.
@@ -211,14 +211,10 @@ function getMetric(
211211
*
212212
*
213213
*/
214-
export function metricsToRequest(exportArgs: ResourceMetrics) {
215-
type WithSyncAttributes = {_syncAttributes: {[index: string]: string}};
216-
const resourcesWithSyncAttributes =
217-
exportArgs.resource as unknown as WithSyncAttributes;
218-
const projectId =
219-
resourcesWithSyncAttributes._syncAttributes[
220-
'monitored_resource.project_id'
221-
];
214+
export function metricsToRequest(
215+
projectId: string,
216+
exportArgs: ResourceMetrics,
217+
) {
222218
const timeSeriesArray = [];
223219
for (const scopeMetrics of exportArgs.scopeMetrics) {
224220
for (const scopeMetric of scopeMetrics.metrics) {
@@ -297,49 +293,33 @@ export function metricsToRequest(exportArgs: ResourceMetrics) {
297293
* @beta
298294
*/
299295
export class CloudMonitoringExporter extends MetricExporter {
300-
private monitoringClient = new MetricServiceClient();
296+
private client: MetricServiceClient;
301297

302-
export(
298+
constructor(options: ClientOptions) {
299+
super();
300+
if (options && options.apiEndpoint) {
301+
// We want the MetricServiceClient to always hit its default endpoint.
302+
delete options.apiEndpoint;
303+
}
304+
this.client = new MetricServiceClient(options);
305+
}
306+
307+
async export(
303308
metrics: ResourceMetrics,
304309
resultCallback: (result: ExportResult) => void,
305-
): void {
310+
): Promise<void> {
306311
(async () => {
307312
try {
308-
const request = metricsToRequest(metrics);
309-
// In order to manage the "One or more points were written more
310-
// frequently than the maximum sampling period configured for the
311-
// metric." error we should have the metric service client retry a few
312-
// times to ensure the metrics do get written.
313-
//
314-
// We use all the usual retry codes plus INVALID_ARGUMENT (code 3)
315-
// because INVALID ARGUMENT (code 3) corresponds to the maximum
316-
// sampling error.
317-
const retry = new RetryOptions(
318-
[
319-
grpc.status.INVALID_ARGUMENT,
320-
grpc.status.DEADLINE_EXCEEDED,
321-
grpc.status.RESOURCE_EXHAUSTED,
322-
grpc.status.ABORTED,
323-
grpc.status.UNAVAILABLE,
324-
],
325-
{
326-
initialRetryDelayMillis: 5000,
327-
retryDelayMultiplier: 2,
328-
maxRetryDelayMillis: 50000,
329-
},
330-
);
331-
await this.monitoringClient.createTimeSeries(
313+
const projectId = await this.client.getProjectId();
314+
const request = metricsToRequest(projectId, metrics);
315+
await this.client.createServiceTimeSeries(
332316
request as ICreateTimeSeriesRequest,
333-
{
334-
retry,
335-
},
336317
);
337318
// The resultCallback typically accepts a value equal to {code: x}
338319
// for some value x along with other info. When the code is equal to 0
339320
// then the operation completed successfully. When the code is not equal
340-
// to 0 then the operation failed. Open telemetry logs errors to the
341-
// console when the resultCallback passes in non-zero code values and
342-
// logs nothing when the code is 0.
321+
// to 0 then the operation failed. The resultCallback will not log
322+
// anything to the console whether the error code was 0 or not.
343323
resultCallback({code: 0});
344324
} catch (error) {
345325
resultCallback(error as ServiceError);

src/client-side-metrics/gcp-metrics-handler.ts

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import {CloudMonitoringExporter} from './exporter';
1516
import {
1617
IMetricsHandler,
1718
OnAttemptCompleteData,
@@ -20,13 +21,39 @@ import {
2021
import * as Resources from '@opentelemetry/resources';
2122
import * as ResourceUtil from '@google-cloud/opentelemetry-resource-util';
2223
import {PushMetricExporter, View} from '@opentelemetry/sdk-metrics';
24+
import {ClientOptions} from 'google-gax';
2325
const {
2426
Aggregation,
2527
ExplicitBucketHistogramAggregation,
2628
MeterProvider,
2729
Histogram,
2830
PeriodicExportingMetricReader,
2931
} = require('@opentelemetry/sdk-metrics');
32+
import * as os from 'os';
33+
import * as crypto from 'crypto';
34+
35+
/**
36+
* Generates a unique client identifier string.
37+
*
38+
* This function creates a client identifier that incorporates the hostname,
39+
* process ID, and a UUID to ensure uniqueness across different client instances
40+
* and processes. The identifier follows the pattern:
41+
*
42+
* `node-<uuid>-<pid><hostname>`
43+
*
44+
* where:
45+
* - `<uuid>` is a randomly generated UUID (version 4).
46+
* - `<pid>` is the process ID of the current Node.js process.
47+
* - `<hostname>` is the hostname of the machine.
48+
*
49+
* @returns {string} A unique client identifier string.
50+
*/
51+
function generateClientUuid() {
52+
const hostname = os.hostname() || 'localhost';
53+
const currentPid = process.pid || '';
54+
const uuid4 = crypto.randomUUID();
55+
return `node-${uuid4}-${currentPid}${hostname}`;
56+
}
3057

3158
/**
3259
* A collection of OpenTelemetry metric instruments used to record
@@ -47,10 +74,9 @@ interface MetricsInstruments {
4774
* This method gets the open telemetry instruments that will store GCP metrics
4875
* for a particular project.
4976
*
50-
* @param projectId The project for which the instruments will be stored.
5177
* @param exporter The exporter the metrics will be sent to.
5278
*/
53-
function createInstruments(projectId: string, exporter: PushMetricExporter) {
79+
function createInstruments(exporter: PushMetricExporter): MetricsInstruments {
5480
const latencyBuckets = [
5581
0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0, 16.0, 20.0, 25.0, 30.0,
5682
40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0, 300.0, 400.0,
@@ -80,7 +106,6 @@ function createInstruments(projectId: string, exporter: PushMetricExporter) {
80106
views: viewList,
81107
resource: new Resources.Resource({
82108
'service.name': 'Cloud Bigtable Table',
83-
'monitored_resource.project_id': projectId,
84109
}).merge(new ResourceUtil.GcpDetectorSync().detect()),
85110
readers: [
86111
// Register the exporter
@@ -183,11 +208,8 @@ function createInstruments(projectId: string, exporter: PushMetricExporter) {
183208
* associating them with relevant attributes for detailed analysis in Cloud Monitoring.
184209
*/
185210
export class GCPMetricsHandler implements IMetricsHandler {
186-
private exporter: PushMetricExporter;
187-
// The variable below is the singleton map from projects to instrument stacks
188-
// which exists so that we only create one instrument stack per project. This
189-
// will eliminate errors due to the maximum sampling period.
190-
static instrumentsForProject: {[projectId: string]: MetricsInstruments} = {};
211+
private otelInstruments: MetricsInstruments;
212+
private clientUid: string;
191213

192214
/**
193215
* The `GCPMetricsHandler` is responsible for managing and recording
@@ -196,33 +218,11 @@ export class GCPMetricsHandler implements IMetricsHandler {
196218
* (histograms and counters) and exports them to Google Cloud Monitoring
197219
* through the provided `PushMetricExporter`.
198220
*
199-
* @param exporter - The `PushMetricExporter` instance to use for exporting
200-
* metrics to Google Cloud Monitoring. This exporter is responsible for
201-
* sending the collected metrics data to the monitoring backend. The provided exporter must be fully configured, for example the projectId must have been set.
202221
*/
203-
constructor(exporter: PushMetricExporter) {
204-
this.exporter = exporter;
205-
}
206-
207-
/**
208-
* Initializes the OpenTelemetry metrics instruments if they haven't been already.
209-
* Creates and registers metric instruments (histograms and counters) for various Bigtable client metrics.
210-
* Sets up a MeterProvider and configures a PeriodicExportingMetricReader for exporting metrics to Cloud Monitoring.
211-
*
212-
* which will be provided to the exporter in every export call.
213-
*
214-
*/
215-
private getInstruments(projectId: string): MetricsInstruments {
216-
// The projectId is needed per metrics handler because when the exporter is
217-
// used it provides the project id for the name of the time series exported.
218-
// ie. name: `projects/${....['monitored_resource.project_id']}`,
219-
if (!GCPMetricsHandler.instrumentsForProject[projectId]) {
220-
GCPMetricsHandler.instrumentsForProject[projectId] = createInstruments(
221-
projectId,
222-
this.exporter,
223-
);
224-
}
225-
return GCPMetricsHandler.instrumentsForProject[projectId];
222+
constructor(options: ClientOptions) {
223+
this.clientUid = generateClientUuid();
224+
const exporter = new CloudMonitoringExporter(options);
225+
this.otelInstruments = createInstruments(exporter);
226226
}
227227

228228
/**
@@ -231,11 +231,11 @@ export class GCPMetricsHandler implements IMetricsHandler {
231231
* @param {OnOperationCompleteData} data Data related to the completed operation.
232232
*/
233233
onOperationComplete(data: OnOperationCompleteData) {
234-
const otelInstruments = this.getInstruments(data.projectId);
234+
const otelInstruments = this.otelInstruments;
235235
const commonAttributes = {
236236
app_profile: data.metricsCollectorData.app_profile,
237237
method: data.metricsCollectorData.method,
238-
client_uid: data.metricsCollectorData.client_uid,
238+
client_uid: this.clientUid,
239239
client_name: data.client_name,
240240
instanceId: data.metricsCollectorData.instanceId,
241241
table: data.metricsCollectorData.table,
@@ -271,11 +271,11 @@ export class GCPMetricsHandler implements IMetricsHandler {
271271
* @param {OnAttemptCompleteData} data Data related to the completed attempt.
272272
*/
273273
onAttemptComplete(data: OnAttemptCompleteData) {
274-
const otelInstruments = this.getInstruments(data.projectId);
274+
const otelInstruments = this.otelInstruments;
275275
const commonAttributes = {
276276
app_profile: data.metricsCollectorData.app_profile,
277277
method: data.metricsCollectorData.method,
278-
client_uid: data.metricsCollectorData.client_uid,
278+
client_uid: this.clientUid,
279279
status: data.status,
280280
client_name: data.client_name,
281281
instanceId: data.metricsCollectorData.instanceId,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import {IMetricsHandler} from './metrics-handler';
16+
import {
17+
ITabularApiSurface,
18+
OperationMetricsCollector,
19+
} from './operation-metrics-collector';
20+
import {MethodName, StreamingState} from './client-side-metrics-attributes';
21+
22+
/**
23+
* A class for tracing and recording client-side metrics related to Bigtable operations.
24+
*/
25+
export class ClientSideMetricsConfigManager {
26+
private metricsHandlers: IMetricsHandler[];
27+
28+
constructor(handlers: IMetricsHandler[]) {
29+
this.metricsHandlers = handlers;
30+
}
31+
32+
createOperation(
33+
methodName: MethodName,
34+
streaming: StreamingState,
35+
table: ITabularApiSurface,
36+
): OperationMetricsCollector {
37+
return new OperationMetricsCollector(
38+
table,
39+
methodName,
40+
streaming,
41+
this.metricsHandlers,
42+
);
43+
}
44+
}

src/client-side-metrics/metrics-handler.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
// limitations under the License.
1414

1515
import {MethodName, StreamingState} from './client-side-metrics-attributes';
16-
import {grpc} from 'google-gax';
1716

1817
/**
1918
* The interfaces below use undefined instead of null to indicate a metric is
@@ -28,11 +27,9 @@ type IMetricsCollectorData = {
2827
zone?: string;
2928
app_profile?: string;
3029
method: MethodName;
31-
client_uid: string;
3230
};
3331

3432
interface StandardData {
35-
projectId: string;
3633
metricsCollectorData: IMetricsCollectorData;
3734
client_name: string;
3835
streaming: StreamingState;

0 commit comments

Comments
 (0)