Skip to content
Open
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
27 changes: 22 additions & 5 deletions handwritten/spanner/OBSERVABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@ enable OpenTelemetry with appropriate exporters at the startup of your applicati
#### OpenTelemetry Dependencies

Add the following dependencies in your `package.json` or install them directly.

> **Note:** `@google-cloud/spanner` uses the OpenTelemetry **v2** SDK. The versions
> below are the v2-compatible releases. If you are upgrading from an older version
> of this client, see the [OpenTelemetry JS 2.x migration guide](https://github.com/open-telemetry/opentelemetry-js/blob/main/doc/upgrade-to-2.x.md).

```javascript
// Required packages for OpenTelemetry SDKs
"@opentelemetry/sdk-trace-base": "^1.26.0",
"@opentelemetry/sdk-trace-node": "^1.26.0",
"@opentelemetry/sdk-trace-base": "^2.11.0",
"@opentelemetry/sdk-trace-node": "^2.0.0",
"@opentelemetry/resources": "^2.11.0",
"@opentelemetry/semantic-conventions": "^1.30.0",

// Package to use Google Cloud Trace exporter
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",

// Packages to enable gRPC instrumentation
"@opentelemetry/instrumentation": "^0.53.0",
"@opentelemetry/instrumentation-grpc": "^0.53.0",
"@opentelemetry/instrumentation": "^0.222.0",
"@opentelemetry/instrumentation-grpc": "^0.222.0",
```

#### OpenTelemetry Configuration
Expand All @@ -46,11 +53,21 @@ const {
const {
TraceExporter,
} = require('@google-cloud/opentelemetry-cloud-trace-exporter');
const {resourceFromAttributes} = require('@opentelemetry/resources');
const {ATTR_SERVICE_NAME} = require('@opentelemetry/semantic-conventions');
const exporter = new TraceExporter();

// Describe the service that is emitting the traces.
// Note: in OpenTelemetry v2 the `Resource` class was replaced by the
// `resourceFromAttributes` factory function.
const resource = resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-service-name',
});

// Create the tracerProvider that the exporter shall be attached to.
const provider = new NodeTracerProvider({
resource: resource,
sampler: new TraceIdRatioBasedSampler(0.1), // sample 10%
spanProcessors: [new BatchSpanProcessor(exporter)]
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import {trace} from '@opentelemetry/api';
import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc';
import {Resource} from '@opentelemetry/resources';
import {resourceFromAttributes} from '@opentelemetry/resources';
import {ATTR_SERVICE_NAME} from '@opentelemetry/semantic-conventions';
import {
BatchSpanProcessor,
Expand All @@ -45,17 +45,17 @@
url: 'https://test-telemetry.sandbox.googleapis.com',
credentials: grpc.credentials.combineChannelCredentials(
grpc.credentials.createSsl(),
grpc.credentials.createFromGoogleCredential(authenticatedClient as any),

Check warning on line 48 in handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
),
});

const provider = new NodeTracerProvider({
resource: new Resource({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'spanner-node-worker-proxy',
'gcp.project_id': WorkerProxy.PROJECT_ID,
}) as any,

Check warning on line 56 in handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
sampler: new TraceIdRatioBasedSampler(this.TRACE_SAMPLING_RATE),
spanProcessors: [new BatchSpanProcessor(traceExporter as any)],

Check warning on line 58 in handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

provider.register();
Expand All @@ -71,7 +71,7 @@
* Creates the configuration object for the Spanner client for connecting to a
* test GFE, including gRPC channel setup.
*/
public static getSpannerOptions(): any {

Check warning on line 74 in handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
const options: SpannerOptions = {
projectId: WorkerProxy.PROJECT_ID,
servicePath: 'localhost',
Expand Down Expand Up @@ -106,7 +106,7 @@
this.TEST_HOST_IN_CERT;
}

(options as any).grpcOptions = grpcOptions;

Check warning on line 109 in handwritten/spanner/google-cloud-spanner-executor/src/cloud-util.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

return options;
}
Expand Down
10 changes: 5 additions & 5 deletions handwritten/spanner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@
"@babel/traverse": "7.27.7",
"@google-cloud/common": "^6.0.0",
"@google-cloud/monitoring": "^5.0.0",
"@google-cloud/opentelemetry-resource-util": "^2.4.0",
"@google-cloud/precise-date": "^5.0.0",
"@google-cloud/promisify": "^5.0.0",
"@google-cloud/spanner-api": "^0.2.0",
Expand All @@ -65,8 +64,9 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/core": "^2.0.0",
"@opentelemetry/resources": "^1.8.0",
"@opentelemetry/sdk-metrics": "^1.30.1",
"@opentelemetry/resource-detector-gcp": "^0.57.0",
"@opentelemetry/resources": "^2.11.0",
"@opentelemetry/sdk-metrics": "^2.11.0",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@types/big.js": "^6.2.2",
"@types/stack-trace": "^0.0.33",
Expand All @@ -87,9 +87,9 @@
},
"devDependencies": {
"@grpc/reflection": "^1.0.4",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.11.0",
"@opentelemetry/sdk-trace-node": "^2.0.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
"@types/concat-stream": "^2.0.3",
"@types/extend": "^3.0.4",
"@types/is": "^0.0.25",
Expand Down
42 changes: 18 additions & 24 deletions handwritten/spanner/src/metrics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@
// 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.
import {
View,
ExplicitBucketHistogramAggregation,
} from '@opentelemetry/sdk-metrics';
import {AggregationType, ViewOptions} from '@opentelemetry/sdk-metrics';

export const SPANNER_METER_NAME = 'spanner-nodejs';
export const CLIENT_METRICS_PREFIX = 'spanner.googleapis.com/internal/client';
Expand Down Expand Up @@ -91,33 +88,30 @@ export const HISTOGRAM_BUCKET_BOUNDARIES = [
];

// Defined Views for metric aggregation
export const OPERATION_LATENCY_VIEW = new View({
const HISTOGRAM_AGGREGATION = {
type: AggregationType.EXPLICIT_BUCKET_HISTOGRAM as const,
options: {boundaries: HISTOGRAM_BUCKET_BOUNDARIES},
};

export const OPERATION_LATENCY_VIEW: ViewOptions = {
instrumentName: METRIC_NAME_OPERATION_LATENCIES,
aggregation: new ExplicitBucketHistogramAggregation(
HISTOGRAM_BUCKET_BOUNDARIES,
),
});
aggregation: HISTOGRAM_AGGREGATION,
};

export const ATTEMPT_LATENCY_VIEW = new View({
export const ATTEMPT_LATENCY_VIEW: ViewOptions = {
instrumentName: METRIC_NAME_ATTEMPT_LATENCIES,
aggregation: new ExplicitBucketHistogramAggregation(
HISTOGRAM_BUCKET_BOUNDARIES,
),
});
aggregation: HISTOGRAM_AGGREGATION,
};

export const GFE_LATENCY_VIEW = new View({
export const GFE_LATENCY_VIEW: ViewOptions = {
instrumentName: METRIC_NAME_GFE_LATENCIES,
aggregation: new ExplicitBucketHistogramAggregation(
HISTOGRAM_BUCKET_BOUNDARIES,
),
});
aggregation: HISTOGRAM_AGGREGATION,
};

export const AFE_LATENCY_VIEW = new View({
export const AFE_LATENCY_VIEW: ViewOptions = {
instrumentName: METRIC_NAME_AFE_LATENCIES,
aggregation: new ExplicitBucketHistogramAggregation(
HISTOGRAM_BUCKET_BOUNDARIES,
),
});
aggregation: HISTOGRAM_AGGREGATION,
};
Comment thread
alkatrivedi marked this conversation as resolved.

export const METRIC_VIEWS = [
OPERATION_LATENCY_VIEW,
Expand Down
13 changes: 13 additions & 0 deletions handwritten/spanner/src/metrics/external-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,16 @@ export enum ValueType {
DOUBLE = 'DOUBLE',
DISTRIBUTION = 'DISTRIBUTION',
}

/**
* A Google Cloud Monitoring monitored resource.
*
* Previously imported from `@google-cloud/opentelemetry-resource-util`, which is
* deprecated and scheduled for archival. The interface is a plain data shape, so
* it is declared locally instead.
* See https://cloud.google.com/monitoring/api/ref_v3/rest/v3/MonitoredResource
*/
export interface MonitoredResource {
type: string;
labels: {[key: string]: string};
}
13 changes: 8 additions & 5 deletions handwritten/spanner/src/metrics/metrics-tracer-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ import * as os from 'os';
import * as process from 'process';
import {MeterProvider, MetricReader} from '@opentelemetry/sdk-metrics';
import {Counter, Histogram, context, ROOT_CONTEXT} from '@opentelemetry/api';
import {detectResources, Resource} from '@opentelemetry/resources';
import {GcpDetectorSync} from '@google-cloud/opentelemetry-resource-util';
import {
detectResources,
resourceFromAttributes,
} from '@opentelemetry/resources';
import {gcpDetector} from '@opentelemetry/resource-detector-gcp';
import * as Constants from './constants';
import {MetricsTracer} from './metrics-tracer';
const version = require('../../../package.json').version;
Expand Down Expand Up @@ -122,7 +125,7 @@ export class MetricsTracerFactory {
*/
public getMeterProvider(readers: MetricReader[] = []): MeterProvider {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method could silently ignore the readers that are passed in if it is called after _meterProvider is initialized.

Test case for verification:

import * as assert from 'assert';
import * as sinon from 'sinon';
import {grpc} from 'google-gax';
import * as mock from '../mockserver/mockspanner';
import {Spanner} from '../../src';
import {MetricsTracerFactory} from '../../src/metrics/metrics-tracer-factory';
import {MetricReader} from '@opentelemetry/sdk-metrics';

class InMemoryMetricReader extends MetricReader {
  protected async onForceFlush(): Promise<void> {}
  protected async onShutdown(): Promise<void> {}
}

it('should bind MetricReader passed to getMeterProvider([reader]) even if getMeterProvider() was called earlier', async () => {
  const sandbox = sinon.createSandbox();
  const server = new grpc.Server();
  const spannerMock = mock.createMockSpanner(server);
  const port = await new Promise<number>((resolve, reject) => {
    server.bindAsync('0.0.0.0:0', grpc.ServerCredentials.createInsecure(), (err, p) =>
      err ? reject(err) : resolve(p)
    );
  });
  spannerMock.putStatementResult(
    'SELECT 1',
    mock.StatementResult.resultSet(mock.createSimpleResultSet())
  );

  try {
    await MetricsTracerFactory.resetInstance();
    sandbox.stub(MetricsTracerFactory as any, '_detectClientLocation').resolves('us-central1');

    const spanner = new Spanner({
      projectId: 'test-project',
      servicePath: 'localhost',
      port,
      sslCreds: grpc.credentials.createInsecure(),
    });
    (spanner as any)._metricsEnabled = true;
    MetricsTracerFactory.enabled = true;

    const factory = MetricsTracerFactory.getInstance('test-project')!;
    // 1. Call getMeterProvider() first (initializes _meterProvider with readers: [])
    factory.getMeterProvider();

    // 2. Now pass a reader (simulating configureMetrics_ async callback completing afterwards)
    const reader = new InMemoryMetricReader();
    factory.getMeterProvider([reader]);

    const database = spanner.instance('inst').database('db');
    await database.run('SELECT 1');

    // FAILS in current PR: throws "MetricReader is not bound to a MetricProducer"
    const {resourceMetrics} = await reader.collect();
    assert.ok(resourceMetrics.scopeMetrics.length > 0);

    await database.close();
    await spanner.close();
  } finally {
    sandbox.restore();
    server.tryShutdown(() => {});
    await MetricsTracerFactory.resetInstance();
  }
});

if (this._meterProvider === null) {
const resource = new Resource({
const resource = resourceFromAttributes({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A race condition between _detectClientLocation() and getMeterProvider() could cause exported location to always be 'global'.

Test case:

import * as assert from 'assert';
import * as sinon from 'sinon';
import {grpc} from 'google-gax';
import * as mock from '../mockserver/mockspanner';
import {Spanner} from '../../src';
import {MetricsTracerFactory} from '../../src/metrics/metrics-tracer-factory';
import {transformResourceMetricToTimeSeriesArray} from '../../src/metrics/transform';
import {MetricReader} from '@opentelemetry/sdk-metrics';

class InMemoryMetricReader extends MetricReader {
  protected async onForceFlush(): Promise<void> {}
  protected async onShutdown(): Promise<void> {}
}

it('should export the detected GCP location rather than global when _detectClientLocation resolves asynchronously', async () => {
  const sandbox = sinon.createSandbox();
  const server = new grpc.Server();
  const spannerMock = mock.createMockSpanner(server);
  const port = await new Promise<number>((resolve, reject) => {
    server.bindAsync('0.0.0.0:0', grpc.ServerCredentials.createInsecure(), (err, p) =>
      err ? reject(err) : resolve(p)
    );
  });
  spannerMock.putStatementResult(
    'SELECT 1',
    mock.StatementResult.resultSet(mock.createSimpleResultSet())
  );

  let resolveLocation!: (loc: string) => void;
  const locationPromise = new Promise<string>(resolve => {
    resolveLocation = resolve;
  });

  try {
    await MetricsTracerFactory.resetInstance();
    sandbox
      .stub(MetricsTracerFactory as any, '_detectClientLocation')
      .returns(locationPromise);

    const spanner = new Spanner({
      projectId: 'test-project',
      servicePath: 'localhost',
      port,
      sslCreds: grpc.credentials.createInsecure(),
    });
    (spanner as any)._metricsEnabled = true;
    MetricsTracerFactory.enabled = true;

    // 1. Replicate exact synchronous sequence from configureMetrics_ (src/index.ts:1691-1700):
    // getMeterProvider runs synchronously while locationPromise is still pending
    const reader = new InMemoryMetricReader();
    const factory = MetricsTracerFactory.getInstance('test-project')!;
    factory.getMeterProvider([reader]);

    // 2. Deterministically resolve locationPromise now and flush microtasks
    resolveLocation('us-central1');
    await locationPromise;
    await Promise.resolve();
    assert.strictEqual((factory as any)._location, 'us-central1');

    const database = spanner.instance('inst').database('db');
    await database.run('SELECT 1');

    const {resourceMetrics} = await reader.collect();
    const timeSeries = transformResourceMetricToTimeSeriesArray(resourceMetrics, 'test-project');
    const exportedLocation = timeSeries[0].resource.labels.location;

    // FAILS in current PR: exportedLocation is 'global' instead of 'us-central1'
    assert.strictEqual(exportedLocation, 'us-central1');

    await database.close();
    await spanner.close();
  } finally {
    sandbox.restore();
    server.tryShutdown(() => {});
    await MetricsTracerFactory.resetInstance();
  }
});

[Constants.MONITORED_RES_LABEL_KEY_PROJECT]: this._projectId,
[Constants.MONITORED_RES_LABEL_KEY_CLIENT_HASH]: this._clientHash,
[Constants.MONITORED_RES_LABEL_KEY_LOCATION]: this._location,
Expand Down Expand Up @@ -454,14 +457,14 @@ export class MetricsTracerFactory {

/**
* Gets the location (region) of the client, otherwise returns to the "global" region.
* Uses GcpDetectorSync to detect the region from the environment.
* Uses the GCP resource detector to detect the region from the environment.
* @returns The detected region string, or "global" if not found.
*/
private static async _detectClientLocation(): Promise<string> {
const defaultRegion = 'global';
try {
const resource = await detectResources({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could fail to detect the location correctly on zonal GKE clusters (it should also check the cloud.availability_zone attribute). Cross-check with the Java client how that one does this.

Test case (albeit a bit 'fake', as we cannot really replicate the behavior of GKE in a test case):

import * as assert from 'assert';
import * as sinon from 'sinon';
import {gcpDetector} from '@opentelemetry/resource-detector-gcp';
import {MetricsTracerFactory} from '../../src/metrics/metrics-tracer-factory';

it('should detect client location on a zonal GKE cluster where cloud.availability_zone is set', async () => {
  const sandbox = sinon.createSandbox();
  try {
    // On a zonal GKE cluster, gcpDetector sets cloud.availability_zone and leaves cloud.region undefined
    sandbox.stub(gcpDetector, 'detect').returns({
      attributes: {
        'cloud.platform': Promise.resolve('gcp_kubernetes_engine'),
        'cloud.availability_zone': Promise.resolve('us-central1-a'),
      },
    });

    const detectedLocation = await (MetricsTracerFactory as any)._detectClientLocation();

    // FAILS in current PR: returns 'global' instead of 'us-central1-a'
    assert.strictEqual(detectedLocation, 'us-central1-a');
  } finally {
    sandbox.restore();
  }
});

detectors: [new GcpDetectorSync()],
detectors: [gcpDetector],
});

await resource?.waitForAsyncAttributes?.();
Expand Down
3 changes: 1 addition & 2 deletions handwritten/spanner/src/metrics/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import {
ResourceMetrics,
} from '@opentelemetry/sdk-metrics';
import {Resource} from '@opentelemetry/resources';
import {MonitoredResource} from '@google-cloud/opentelemetry-resource-util';
import * as path from 'path';
import {MetricKind, ValueType} from './external-types';
import {MetricKind, MonitoredResource, ValueType} from './external-types';
import {
SPANNER_METER_NAME,
CLIENT_METRICS_PREFIX,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
.onThirdCall()
.returns({add: addGfeConnectivityErrorCountStub});

sandbox.stub(MeterProvider.prototype, 'getMeter').returns(meterStub as any);

Check warning on line 67 in handwritten/spanner/test/metrics/metrics-tracer-factory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

// metrics provider and related objects
mockExporter = sandbox.createStubInstance(CloudMonitoringMetricsExporter);
Expand All @@ -79,13 +79,11 @@
MetricsTracerFactory.enabled = true;
sandbox.resetHistory();
await MetricsTracerFactory.resetInstance();
const provider =
MetricsTracerFactory.getInstance('project-id')!.getMeterProvider();
const reader = new PeriodicExportingMetricReader({
exporter: mockExporter,
exportIntervalMillis: 60000,
});
provider.addMetricReader(reader);
MetricsTracerFactory.getInstance('project-id')!.getMeterProvider([reader]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now triggers a real network call to try to detect the GCP location. And while that does not fail, it does (probably) emit a warning in the error log when running the test (and potentially also causes the test to be slower). So it should probably also be mocked in this test:

  beforeEach(async () => {
    MetricsTracerFactory.enabled = true;
    sandbox.resetHistory();
    sandbox
      .stub(MetricsTracerFactory as any, '_detectClientLocation')
      .resolves('global');
    await MetricsTracerFactory.resetInstance();
    ...

});

afterEach(async () => {
Expand Down Expand Up @@ -166,12 +164,12 @@
'1.1a2bc3d4.1.1.1.1',
);

assert.strictEqual((factory as any)._currentOperationTracers.size, 1);

Check warning on line 167 in handwritten/spanner/test/metrics/metrics-tracer-factory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

factory!.clearCurrentTracer('1.1a2bc3d4.1.1.1');

assert.strictEqual((factory as any)._currentOperationTracers.size, 0);

Check warning on line 171 in handwritten/spanner/test/metrics/metrics-tracer-factory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
assert.strictEqual((factory as any)._currentOperationLastUpdatedMs.size, 0);

Check warning on line 172 in handwritten/spanner/test/metrics/metrics-tracer-factory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

it('should correctly set default attributes', () => {
Expand Down Expand Up @@ -199,7 +197,7 @@
describe('getInstanceAttributes', () => {
let factory: MetricsTracerFactory;
beforeEach(() => {
factory = new (MetricsTracerFactory as any)();

Check warning on line 200 in handwritten/spanner/test/metrics/metrics-tracer-factory.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
});

afterEach(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
} from '../../src/metrics/constants';
import {Counter, Meter, Histogram} from '@opentelemetry/api';
import {ExportResult, ExportResultCode} from '@opentelemetry/core';
import {Resource} from '@opentelemetry/resources';
import {resourceFromAttributes} from '@opentelemetry/resources';

const PROJECT_ID = 'test-project';
const INSTANCE_ID = 'test-instance';
Expand Down Expand Up @@ -95,7 +95,7 @@ describe('Export', () => {
beforeEach(() => {
exporter = new CloudMonitoringMetricsExporter({auth}, PROJECT_ID);
reader = new InMemoryMetricReader();
const resource = new Resource({
const resource = resourceFromAttributes({
['project_id']: PROJECT_ID,
['client_hash']: CLIENT_HASH,
['location']: LOCATION,
Expand Down
4 changes: 2 additions & 2 deletions handwritten/spanner/test/metrics/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
MeterProvider,
MetricReader,
} from '@opentelemetry/sdk-metrics';
import {Resource} from '@opentelemetry/resources';
import {Resource, resourceFromAttributes} from '@opentelemetry/resources';
import {
Attributes,
Counter,
Expand Down Expand Up @@ -84,7 +84,7 @@ describe('transform', () => {
sandbox.stub(MetricsTracerFactory, 'getInstance').returns(mockFactory);

reader = new InMemoryMetricReader();
resource = new Resource({
resource = resourceFromAttributes({
['project_id']: 'project_id',
['client_hash']: 'test_hash',
['location']: 'test_location',
Expand Down
Loading
Loading