fix: Correctly determine project ID for metrics export by surbhigarg92 · Pull Request #2427 · googleapis/nodejs-spanner · GitHub
Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions src/index.ts
10 changes: 9 additions & 1 deletion src/metrics/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,15 @@ export const MetricInterceptor = (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: function (metadata, listener, next) {
// Record attempt metric on request start
const factory = MetricsTracerFactory.getInstance();
const resourcePrefix = metadata.get(
'google-cloud-resource-prefix',
)[0] as string;
const match = resourcePrefix?.match(/^projects\/([^/]+)\//);
const projectId = match ? match[1] : undefined;
let factory;
if (projectId) {
factory = MetricsTracerFactory.getInstance(projectId);
}
const requestId = metadata.get('x-goog-spanner-request-id')[0] as string;
const metricsTracer = factory?.getCurrentTracer(requestId);
metricsTracer?.recordAttemptStart();
Expand Down
8 changes: 4 additions & 4 deletions src/metrics/metrics-tracer-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export class MetricsTracerFactory {
private _currentOperationTracers = new Map();
private _currentOperationLastUpdatedMs = new Map();
private _intervalTracerCleanup: NodeJS.Timeout;
public static _readers: MetricReader[] = [];
public static enabled = true;

/**
Expand Down Expand Up @@ -101,7 +100,7 @@ export class MetricsTracerFactory {
* @param projectId Optional GCP project ID for the factory instantiation. Does nothing for subsequent calls.
* @returns The singleton MetricsTracerFactory instance or null if disabled.
*/
public static getInstance(projectId = ''): MetricsTracerFactory | null {
public static getInstance(projectId: string): MetricsTracerFactory | null {
if (!MetricsTracerFactory.enabled) {
return null;
}
Expand All @@ -110,6 +109,7 @@ export class MetricsTracerFactory {
if (MetricsTracerFactory._instance === null) {
MetricsTracerFactory._instance = new MetricsTracerFactory(projectId);
}

return MetricsTracerFactory!._instance;
}

Expand All @@ -128,7 +128,6 @@ export class MetricsTracerFactory {
[Constants.MONITORED_RES_LABEL_KEY_INSTANCE]: 'unknown',
[Constants.MONITORED_RES_LABEL_KEY_INSTANCE_CONFIG]: 'unknown',
});
MetricsTracerFactory._readers = readers;
this._meterProvider = new MeterProvider({
resource: resource,
readers: readers,
Expand All @@ -142,7 +141,7 @@ export class MetricsTracerFactory {
/**
* Resets the singleton instance of the MetricsTracerFactory.
*/
public static async resetInstance() {
public static async resetInstance(projectId?: string) {
clearInterval(MetricsTracerFactory._instance?._intervalTracerCleanup);
await MetricsTracerFactory._instance?.resetMeterProvider();
MetricsTracerFactory._instance = null;
Expand Down Expand Up @@ -250,6 +249,7 @@ export class MetricsTracerFactory {
MetricsTracerFactory.enabled,
database,
instance,
this._projectId,
method,
operationRequest,
);
Expand Down
5 changes: 4 additions & 1 deletion src/metrics/metrics-tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export class MetricsTracer {
public enabled: boolean,
private _database: string,
private _instance: string,
private _projectId: string,
private _methodName: string,
private _request: string,
) {
Expand Down Expand Up @@ -276,7 +277,9 @@ export class MetricsTracer {
operationLatencyMilliseconds,
operationAttributes,
);
MetricsTracerFactory.getInstance()!.clearCurrentTracer(this._request);
MetricsTracerFactory.getInstance(this._projectId)!.clearCurrentTracer(
this._request,
);
}

/**
Expand Down
26 changes: 7 additions & 19 deletions src/metrics/spanner-metrics-exporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,15 @@ export const MAX_BATCH_EXPORT_SIZE = 200;
* Format and sends metrics information to Google Cloud Monitoring.
*/
export class CloudMonitoringMetricsExporter implements PushMetricExporter {
private _projectId: string | void | Promise<string | void>;
private _projectId: string;
private _lastExported: Date = new Date(0);
private readonly _client: MetricServiceClient;
private _metricsExportFailureLogged = false;

constructor({auth}: ExporterOptions) {
constructor({auth}: ExporterOptions, projectId: string) {
this._client = new MetricServiceClient({auth: auth});

// Start this async process as early as possible. It will be
// awaited on the first export because constructors are synchronous
this._projectId = auth.getProjectId().catch(err => {
console.error(err);
});
this._projectId = projectId;
}

/**
Expand Down Expand Up @@ -83,18 +79,10 @@ export class CloudMonitoringMetricsExporter implements PushMetricExporter {
private async _exportAsync(
resourceMetrics: ResourceMetrics,
): Promise<ExportResult> {
if (this._projectId instanceof Promise) {
this._projectId = await this._projectId;
}

if (!this._projectId) {
const error = new Error('expecting a non-blank ProjectID');
console.error(error.message);
return {code: ExportResultCode.FAILED, error};
}

const timeSeriesList =
transformResourceMetricToTimeSeriesArray(resourceMetrics);
const timeSeriesList = transformResourceMetricToTimeSeriesArray(
resourceMetrics,
this._projectId,
);

let failure: {sendFailed: false} | {sendFailed: true; error: Error} = {
sendFailed: false,
Expand Down
17 changes: 11 additions & 6 deletions src/metrics/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function _transformValueType(metric: MetricData): ValueType {
*/
export function transformResourceMetricToTimeSeriesArray(
resourceMetrics: ResourceMetrics,
projectId: string,
) {
const resource = resourceMetrics?.resource;
const scopeMetrics = resourceMetrics?.scopeMetrics;
Expand All @@ -107,7 +108,7 @@ export function transformResourceMetricToTimeSeriesArray(
// Flatmap the data points in each metric to create a TimeSeries for each point
.flatMap(metric =>
metric.dataPoints.flatMap(dataPoint =>
_createTimeSeries(metric, dataPoint, resource),
_createTimeSeries(metric, dataPoint, resource, projectId),
),
)
);
Expand All @@ -119,14 +120,15 @@ export function transformResourceMetricToTimeSeriesArray(
function _createTimeSeries<T>(
metric: MetricData,
dataPoint: DataPoint<T>,
resource?: Resource,
resource: Resource,
projectId: string,
) {
const type = path.posix.join(CLIENT_METRICS_PREFIX, metric.descriptor.name);
const resourceLabels = resource
? _extractLabels(resource)
? _extractLabels(resource, projectId)
: {metricLabels: {}, monitoredResourceLabels: {}};

const dataLabels = _extractLabels(dataPoint);
const dataLabels = _extractLabels(dataPoint, projectId);

const labels = {
...resourceLabels.metricLabels,
Expand Down Expand Up @@ -205,8 +207,11 @@ function _transformPoint<T>(metric: MetricData, dataPoint: DataPoint<T>) {
}

/** Extracts metric and monitored resource labels from data point */
function _extractLabels<T>({attributes = {}}: DataPoint<T> | Resource) {
const factory = MetricsTracerFactory.getInstance();
function _extractLabels<T>(
{attributes = {}}: DataPoint<T> | Resource,
projectId: string,
) {
const factory = MetricsTracerFactory.getInstance(projectId);
// Add Client name and Client UID metric labels
attributes[METRIC_LABEL_KEY_CLIENT_UID] =
factory?.clientUid ?? UNKNOWN_ATTRIBUTE;
Expand Down
32 changes: 21 additions & 11 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,16 @@ assert.strictEqual(CLOUD_RESOURCE_HEADER, 'google-cloud-resource-prefix');
const apiConfig = require('../src/spanner_grpc_config.json');

async function disableMetrics(sandbox: sinon.SinonSandbox) {
if (
Object.prototype.hasOwnProperty.call(
process.env,
'SPANNER_DISABLE_BUILTIN_METRICS',
)
) {
sandbox.replace(process.env, 'SPANNER_DISABLE_BUILTIN_METRICS', 'true');
} else {
sandbox.define(process.env, 'SPANNER_DISABLE_BUILTIN_METRICS', 'true');
}
sandbox.stub(process.env, 'SPANNER_DISABLE_BUILTIN_METRICS').value('true');
await MetricsTracerFactory.resetInstance();
MetricsTracerFactory.enabled = false;
}

async function enableMetrics(sandbox: sinon.SinonSandbox) {
sandbox.stub(process.env, 'SPANNER_DISABLE_BUILTIN_METRICS').value('false');
await MetricsTracerFactory.resetInstance();
}

function getFake(obj: {}) {
return obj as {
calledWith_: IArguments;
Expand Down Expand Up @@ -133,7 +129,14 @@ const fakeV1: any = {
function fakeGoogleAuth() {
return {
calledWith_: arguments,
getProjectId: () => Promise.resolve('project-id'),
getProjectId: function (callback) {
if (callback) {
callback(null, 'project-id');
return;
} else {
return Promise.resolve('project-id');
}
},
};
}

Expand Down Expand Up @@ -328,6 +331,13 @@ describe('Spanner', () => {
assert.strictEqual(spanner.routeToLeaderEnabled, false);
});

it('should configure metrics if project Id is not passed', async () => {
await enableMetrics(sandbox);
const spanner = new Spanner();
assert.strictEqual(MetricsTracerFactory.enabled, true);
await disableMetrics(sandbox);
});

it('should optionally accept disableBuiltInMetrics', () => {
const spanner = new Spanner({disableBuiltInMetrics: true});
assert.strictEqual(MetricsTracerFactory.enabled, false);
Expand Down
4 changes: 4 additions & 0 deletions test/metrics/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ describe('MetricInterceptor', () => {
},
};
testMetadata = new grpc.Metadata();
testMetadata.set(
'google-cloud-resource-prefix',
'projects/test-project/instances/instance/databases/database-1',
);
});

afterEach(() => {
Expand Down
8 changes: 4 additions & 4 deletions test/metrics/metrics-tracer-factory.ts
Loading
Loading