Skip to content

Commit 8b23d43

Browse files
Merge pull request #6854 from Shopify/dlm-handle-metrics-errors
Do not crash on metrics errors
2 parents fa649f5 + d588adf commit 8b23d43

File tree

2 files changed

+78
-6
lines changed

2 files changed

+78
-6
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {InstantaneousMetricReader} from './InstantaneousMetricReader.js'
2+
import {ExportResultCode} from '@opentelemetry/core'
3+
import type {PushMetricExporter, ResourceMetrics} from '@opentelemetry/sdk-metrics'
4+
import {MeterProvider} from '@opentelemetry/sdk-metrics'
5+
import {describe, expect, test, vi} from 'vitest'
6+
import {diag} from '@opentelemetry/api'
7+
8+
vi.mock('@opentelemetry/api')
9+
10+
function createMockExporter(resultCode: ExportResultCode, error?: Error): PushMetricExporter {
11+
return {
12+
export: vi.fn((_metrics: ResourceMetrics, callback: (result: {code: ExportResultCode; error?: Error}) => void) => {
13+
callback({code: resultCode, error})
14+
}),
15+
forceFlush: vi.fn().mockResolvedValue(undefined),
16+
shutdown: vi.fn().mockResolvedValue(undefined),
17+
} as unknown as PushMetricExporter
18+
}
19+
20+
function createReaderWithProvider(exporter: PushMetricExporter): {
21+
reader: InstantaneousMetricReader
22+
provider: MeterProvider
23+
} {
24+
const reader = new InstantaneousMetricReader({exporter, throttleLimit: 0})
25+
const provider = new MeterProvider()
26+
provider.addMetricReader(reader)
27+
return {reader, provider}
28+
}
29+
30+
describe('InstantaneousMetricReader', () => {
31+
test('logs errors when metrics collection fails', async () => {
32+
const exporter = createMockExporter(ExportResultCode.SUCCESS)
33+
const {reader, provider} = createReaderWithProvider(exporter)
34+
35+
const collectionError = new Error('Collection failed')
36+
vi.spyOn(reader as any, 'collect').mockResolvedValue({
37+
resourceMetrics: {resource: {}, scopeMetrics: []},
38+
errors: [collectionError],
39+
})
40+
41+
await expect(reader.forceFlush()).resolves.toBeUndefined()
42+
expect(diag.error).toHaveBeenCalledWith('InstantaneousMetricReader: metrics collection errors', collectionError)
43+
await provider.shutdown()
44+
})
45+
46+
test('resolves on successful export', async () => {
47+
const exporter = createMockExporter(ExportResultCode.SUCCESS)
48+
const {reader, provider} = createReaderWithProvider(exporter)
49+
50+
await expect(reader.forceFlush()).resolves.toBeUndefined()
51+
expect(diag.error).not.toHaveBeenCalled()
52+
await provider.shutdown()
53+
})
54+
55+
test('resolves without rejecting on export failure', async () => {
56+
const err = new Error('Export failed with retryable status')
57+
const exporter = createMockExporter(ExportResultCode.FAILED, err)
58+
const {reader, provider} = createReaderWithProvider(exporter)
59+
60+
await expect(reader.forceFlush()).resolves.toBeUndefined()
61+
expect(diag.error).toHaveBeenCalledWith('InstantaneousMetricReader: metrics export failed', err)
62+
await provider.shutdown()
63+
})
64+
65+
test('resolves without rejecting when export error is undefined', async () => {
66+
const exporter = createMockExporter(ExportResultCode.FAILED)
67+
const {reader, provider} = createReaderWithProvider(exporter)
68+
69+
await expect(reader.forceFlush()).resolves.toBeUndefined()
70+
expect(diag.error).toHaveBeenCalledWith('InstantaneousMetricReader: metrics export failed', undefined)
71+
await provider.shutdown()
72+
})
73+
})

packages/cli-kit/src/public/node/vendor/otel-js/export/InstantaneousMetricReader.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,15 @@ export class InstantaneousMetricReader extends MetricReader {
3838
const {resourceMetrics, errors} = await this.collect({})
3939

4040
if (errors.length > 0) {
41-
diag.error('PeriodicExportingMetricReader: metrics collection errors', ...errors)
41+
diag.error('InstantaneousMetricReader: metrics collection errors', ...errors)
4242
}
4343

44-
return new Promise((resolve, reject) => {
44+
return new Promise((resolve) => {
4545
this._exporter.export(resourceMetrics, (result) => {
46-
if (result.code === ExportResultCode.SUCCESS) {
47-
resolve()
48-
} else {
49-
reject(result.error ?? new Error(`InstantaneousMetricReader: metrics export failed (error ${result.error})`))
46+
if (result.code !== ExportResultCode.SUCCESS) {
47+
diag.error('InstantaneousMetricReader: metrics export failed', result.error)
5048
}
49+
resolve()
5150
})
5251
})
5352
}

0 commit comments

Comments
 (0)