Skip to content

Commit 3ac07de

Browse files
committed
feat(deno): add denoRuntimeMetricsIntegration
1 parent 4a43db2 commit 3ac07de

3 files changed

Lines changed: 300 additions & 0 deletions

File tree

packages/deno/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,4 @@ export { contextLinesIntegration } from './integrations/contextlines';
109109
export { denoCronIntegration } from './integrations/deno-cron';
110110
export { breadcrumbsIntegration } from './integrations/breadcrumbs';
111111
export { vercelAIIntegration } from './integrations/tracing/vercelai';
112+
export { denoRuntimeMetricsIntegration, type DenoRuntimeMetricsOptions } from './integrations/denoRuntimeMetrics';
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { _INTERNAL_safeDateNow, _INTERNAL_safeUnref, defineIntegration, metrics } from '@sentry/core';
2+
3+
const INTEGRATION_NAME = 'DenoRuntimeMetrics';
4+
const DEFAULT_INTERVAL_MS = 30_000;
5+
6+
export interface DenoRuntimeMetricsOptions {
7+
/**
8+
* Which metrics to collect.
9+
*
10+
* Default on (4 metrics):
11+
* - `memRss` — Resident Set Size (actual memory footprint)
12+
* - `memHeapUsed` — V8 heap currently in use
13+
* - `memHeapTotal` — total V8 heap allocated
14+
* - `uptime` — process uptime (detect restarts/crashes)
15+
*
16+
* Default off (opt-in):
17+
* - `memExternal` — external memory (JS objects outside the V8 isolate)
18+
*
19+
* Note: CPU utilization and event loop metrics are not available in Deno.
20+
*/
21+
collect?: {
22+
memRss?: boolean;
23+
memHeapUsed?: boolean;
24+
memHeapTotal?: boolean;
25+
memExternal?: boolean;
26+
uptime?: boolean;
27+
};
28+
/**
29+
* How often to collect metrics, in milliseconds.
30+
* @default 30000
31+
*/
32+
collectionIntervalMs?: number;
33+
}
34+
35+
/**
36+
* Automatically collects Deno runtime metrics and emits them to Sentry.
37+
*
38+
* @example
39+
* ```ts
40+
* Sentry.init({
41+
* integrations: [
42+
* Sentry.denoRuntimeMetricsIntegration(),
43+
* ],
44+
* });
45+
* ```
46+
*/
47+
export const denoRuntimeMetricsIntegration = defineIntegration((options: DenoRuntimeMetricsOptions = {}) => {
48+
const collectionIntervalMs = options.collectionIntervalMs ?? DEFAULT_INTERVAL_MS;
49+
const collect = {
50+
// Default on
51+
memRss: true,
52+
memHeapUsed: true,
53+
memHeapTotal: true,
54+
uptime: true,
55+
// Default off
56+
memExternal: false,
57+
...options.collect,
58+
};
59+
60+
let intervalId: ReturnType<typeof setInterval> | undefined;
61+
let prevFlushTime: number = 0;
62+
63+
const METRIC_ATTRIBUTES_BYTE = { unit: 'byte', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } };
64+
const METRIC_ATTRIBUTES_SECOND = { unit: 'second', attributes: { 'sentry.origin': 'auto.deno.runtime_metrics' } };
65+
66+
function collectMetrics(): void {
67+
const now = _INTERNAL_safeDateNow();
68+
const elapsed = now - prevFlushTime;
69+
70+
if (collect.memRss || collect.memHeapUsed || collect.memHeapTotal || collect.memExternal) {
71+
const mem = Deno.memoryUsage();
72+
if (collect.memRss) {
73+
metrics.gauge('deno.runtime.mem.rss', mem.rss, METRIC_ATTRIBUTES_BYTE);
74+
}
75+
if (collect.memHeapUsed) {
76+
metrics.gauge('deno.runtime.mem.heap_used', mem.heapUsed, METRIC_ATTRIBUTES_BYTE);
77+
}
78+
if (collect.memHeapTotal) {
79+
metrics.gauge('deno.runtime.mem.heap_total', mem.heapTotal, METRIC_ATTRIBUTES_BYTE);
80+
}
81+
if (collect.memExternal) {
82+
metrics.gauge('deno.runtime.mem.external', mem.external, METRIC_ATTRIBUTES_BYTE);
83+
}
84+
}
85+
86+
if (collect.uptime && elapsed > 0) {
87+
metrics.count('deno.runtime.process.uptime', elapsed / 1000, METRIC_ATTRIBUTES_SECOND);
88+
}
89+
90+
prevFlushTime = now;
91+
}
92+
93+
return {
94+
name: INTEGRATION_NAME,
95+
96+
setup(): void {
97+
prevFlushTime = _INTERNAL_safeDateNow();
98+
99+
// Guard against double setup (e.g. re-init).
100+
if (intervalId) {
101+
clearInterval(intervalId);
102+
}
103+
intervalId = _INTERNAL_safeUnref(setInterval(collectMetrics, collectionIntervalMs));
104+
},
105+
};
106+
});
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// <reference lib="deno.ns" />
2+
3+
import { assertEquals, assertNotEquals } from 'https://deno.land/std@0.212.0/assert/mod.ts';
4+
import { spy, stub } from 'https://deno.land/std@0.212.0/testing/mock.ts';
5+
import { FakeTime } from 'https://deno.land/std@0.212.0/testing/time.ts';
6+
import { denoRuntimeMetricsIntegration, metrics } from '../build/esm/index.js';
7+
8+
const MOCK_MEMORY: Deno.MemoryUsage = {
9+
rss: 50_000_000,
10+
heapTotal: 30_000_000,
11+
heapUsed: 20_000_000,
12+
external: 1_000_000,
13+
};
14+
15+
// deno-lint-ignore no-explicit-any
16+
type AnyCall = { args: any[] };
17+
18+
Deno.test('denoRuntimeMetricsIntegration has the correct name', () => {
19+
const integration = denoRuntimeMetricsIntegration();
20+
assertEquals(integration.name, 'DenoRuntimeMetrics');
21+
});
22+
23+
Deno.test('starts a collection interval', () => {
24+
using time = new FakeTime();
25+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
26+
const gaugeSpy = spy(metrics, 'gauge');
27+
28+
try {
29+
const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 });
30+
integration.setup!({} as never);
31+
32+
assertEquals(gaugeSpy.calls.length, 0);
33+
time.tick(1_000);
34+
assertNotEquals(gaugeSpy.calls.length, 0);
35+
} finally {
36+
gaugeSpy.restore();
37+
}
38+
});
39+
40+
Deno.test('emits default memory metrics', () => {
41+
using time = new FakeTime();
42+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
43+
const gaugeSpy = spy(metrics, 'gauge');
44+
45+
try {
46+
const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 });
47+
integration.setup!({} as never);
48+
time.tick(1_000);
49+
50+
const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]);
51+
assertEquals(names.includes('deno.runtime.mem.rss'), true);
52+
assertEquals(names.includes('deno.runtime.mem.heap_used'), true);
53+
assertEquals(names.includes('deno.runtime.mem.heap_total'), true);
54+
} finally {
55+
gaugeSpy.restore();
56+
}
57+
});
58+
59+
Deno.test('emits correct memory values', () => {
60+
using time = new FakeTime();
61+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
62+
const gaugeSpy = spy(metrics, 'gauge');
63+
64+
try {
65+
const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 });
66+
integration.setup!({} as never);
67+
time.tick(1_000);
68+
69+
const calls = gaugeSpy.calls as AnyCall[];
70+
const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss');
71+
const heapUsedCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_used');
72+
const heapTotalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.heap_total');
73+
74+
assertEquals(rssCall?.args[1], 50_000_000);
75+
assertEquals(heapUsedCall?.args[1], 20_000_000);
76+
assertEquals(heapTotalCall?.args[1], 30_000_000);
77+
} finally {
78+
gaugeSpy.restore();
79+
}
80+
});
81+
82+
Deno.test('does not emit mem.external by default', () => {
83+
using time = new FakeTime();
84+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
85+
const gaugeSpy = spy(metrics, 'gauge');
86+
87+
try {
88+
const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 });
89+
integration.setup!({} as never);
90+
time.tick(1_000);
91+
92+
const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]);
93+
assertEquals(names.includes('deno.runtime.mem.external'), false);
94+
} finally {
95+
gaugeSpy.restore();
96+
}
97+
});
98+
99+
Deno.test('emits mem.external when opted in', () => {
100+
using time = new FakeTime();
101+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
102+
const gaugeSpy = spy(metrics, 'gauge');
103+
104+
try {
105+
const integration = denoRuntimeMetricsIntegration({
106+
collectionIntervalMs: 1_000,
107+
collect: { memExternal: true },
108+
});
109+
integration.setup!({} as never);
110+
time.tick(1_000);
111+
112+
const calls = gaugeSpy.calls as AnyCall[];
113+
const externalCall = calls.find(c => c.args[0] === 'deno.runtime.mem.external');
114+
assertEquals(externalCall?.args[1], 1_000_000);
115+
} finally {
116+
gaugeSpy.restore();
117+
}
118+
});
119+
120+
Deno.test('emits uptime counter', () => {
121+
using time = new FakeTime();
122+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
123+
const countSpy = spy(metrics, 'count');
124+
125+
try {
126+
const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 });
127+
integration.setup!({} as never);
128+
time.tick(1_000);
129+
130+
const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime');
131+
assertNotEquals(uptimeCall, undefined);
132+
} finally {
133+
countSpy.restore();
134+
}
135+
});
136+
137+
Deno.test('respects opt-out: skips mem.rss when memRss is false', () => {
138+
using time = new FakeTime();
139+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
140+
const gaugeSpy = spy(metrics, 'gauge');
141+
142+
try {
143+
const integration = denoRuntimeMetricsIntegration({
144+
collectionIntervalMs: 1_000,
145+
collect: { memRss: false },
146+
});
147+
integration.setup!({} as never);
148+
time.tick(1_000);
149+
150+
const names = (gaugeSpy.calls as AnyCall[]).map(c => c.args[0]);
151+
assertEquals(names.includes('deno.runtime.mem.rss'), false);
152+
} finally {
153+
gaugeSpy.restore();
154+
}
155+
});
156+
157+
Deno.test('skips uptime when uptime is false', () => {
158+
using time = new FakeTime();
159+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
160+
const countSpy = spy(metrics, 'count');
161+
162+
try {
163+
const integration = denoRuntimeMetricsIntegration({
164+
collectionIntervalMs: 1_000,
165+
collect: { uptime: false },
166+
});
167+
integration.setup!({} as never);
168+
time.tick(1_000);
169+
170+
const uptimeCall = (countSpy.calls as AnyCall[]).find(c => c.args[0] === 'deno.runtime.process.uptime');
171+
assertEquals(uptimeCall, undefined);
172+
} finally {
173+
countSpy.restore();
174+
}
175+
});
176+
177+
Deno.test('attaches correct sentry.origin attribute', () => {
178+
using time = new FakeTime();
179+
using _memStub = stub(Deno, 'memoryUsage', () => MOCK_MEMORY);
180+
const gaugeSpy = spy(metrics, 'gauge');
181+
182+
try {
183+
const integration = denoRuntimeMetricsIntegration({ collectionIntervalMs: 1_000 });
184+
integration.setup!({} as never);
185+
time.tick(1_000);
186+
187+
const calls = gaugeSpy.calls as AnyCall[];
188+
const rssCall = calls.find(c => c.args[0] === 'deno.runtime.mem.rss');
189+
assertEquals(rssCall?.args[2]?.attributes?.['sentry.origin'], 'auto.deno.runtime_metrics');
190+
} finally {
191+
gaugeSpy.restore();
192+
}
193+
});

0 commit comments

Comments
 (0)