Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/ci-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ jobs:
tail -n 50 /tmp/iii-engine.log || true
exit 1

- name: Run tests
- name: Run tests with coverage
env:
III_BRIDGE_URL: ws://localhost:49199
III_HTTP_URL: http://localhost:3199
run: pnpm --filter iii-sdk test
run: pnpm --filter iii-sdk test:coverage

- name: Stop III Engine
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
tail -n 50 /tmp/iii-engine.log || true
exit 1

- name: Run tests
- name: Run tests with coverage
run: uv run pytest -q

- name: Stop III Engine
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ worker_py/__pycache__/
.vscode/
.bin/
dist/
.coverage
coverage/
htmlcov/
.DS_Store
.idea/
.cursor/
Expand Down
4 changes: 3 additions & 1 deletion packages/node/iii/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"scripts": {
"build": "tsdown",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
},
"exports": {
Expand Down Expand Up @@ -53,9 +54,10 @@
"ws": "^8.18.3"
},
"devDependencies": {
"@vitest/coverage-v8": "^2.1.0",
"@types/ws": "^8.18.1",
"tsdown": "^0.17.0",
"typescript": "^5.9.3",
"vitest": "^2.1.0"
}
}
}
119 changes: 119 additions & 0 deletions packages/node/iii/tests/otel-worker-gauges.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { WorkerMetricsCollector } from '../src/worker-metrics'
import { registerWorkerGauges, stopWorkerGauges } from '../src/otel-worker-gauges'

type FakeGauge = { name: string }

describe('registerWorkerGauges', () => {
beforeEach(() => {
vi.clearAllMocks()
stopWorkerGauges()
})

it('registers gauges once, records metrics, and unregisters the callback on stop', () => {
const gauges: FakeGauge[] = []
let batchCallback: ((result: { observe: (...args: unknown[]) => void }) => void) | undefined

vi.spyOn(WorkerMetricsCollector.prototype, 'collect').mockReturnValue({
memory_heap_used: 10,
memory_heap_total: 20,
memory_rss: 30,
memory_external: 40,
cpu_percent: 50,
cpu_user_micros: 60,
cpu_system_micros: 70,
event_loop_lag_ms: 80,
uptime_seconds: 90,
timestamp_ms: 123,
runtime: 'node',
})
const stopMonitoringSpy = vi
.spyOn(WorkerMetricsCollector.prototype, 'stopMonitoring')
.mockImplementation(() => {})

const meter = {
createObservableGauge: vi.fn((name: string) => {
const gauge = { name }
gauges.push(gauge)
return gauge
}),
addBatchObservableCallback: vi.fn((callback: typeof batchCallback) => {
batchCallback = callback
}),
removeBatchObservableCallback: vi.fn(),
}
const batchResult = {
observe: vi.fn(),
}

registerWorkerGauges(meter as never, {
workerId: 'worker-123',
workerName: 'coverage-worker',
})
registerWorkerGauges(meter as never, {
workerId: 'worker-ignored',
})

expect(meter.createObservableGauge).toHaveBeenCalledTimes(9)
expect(batchCallback).toBeTypeOf('function')

batchCallback?.(batchResult)

expect(batchResult.observe).toHaveBeenCalledTimes(9)
expect(batchResult.observe).toHaveBeenCalledWith(
gauges[0],
10,
expect.objectContaining({
'worker.id': 'worker-123',
'worker.name': 'coverage-worker',
}),
)

stopWorkerGauges()

expect(meter.removeBatchObservableCallback).toHaveBeenCalledOnce()
expect(stopMonitoringSpy).toHaveBeenCalledOnce()
})

it('skips undefined metrics and handles stop without prior registration', () => {
const gauges: FakeGauge[] = []
let batchCallback: ((result: { observe: (...args: unknown[]) => void }) => void) | undefined

vi.spyOn(WorkerMetricsCollector.prototype, 'collect').mockReturnValue({
memory_heap_used: undefined,
memory_heap_total: 20,
memory_rss: undefined,
memory_external: 40,
cpu_percent: undefined,
cpu_user_micros: 60,
cpu_system_micros: undefined,
event_loop_lag_ms: 80,
uptime_seconds: undefined,
timestamp_ms: 123,
runtime: 'node',
})

const meter = {
createObservableGauge: vi.fn((name: string) => {
const gauge = { name }
gauges.push(gauge)
return gauge
}),
addBatchObservableCallback: vi.fn((callback: typeof batchCallback) => {
batchCallback = callback
}),
removeBatchObservableCallback: vi.fn(),
}
const batchResult = {
observe: vi.fn(),
}

stopWorkerGauges()
registerWorkerGauges(meter as never, { workerId: 'worker-456' })

batchCallback?.(batchResult)

expect(batchResult.observe).toHaveBeenCalledTimes(4)
expect(batchResult.observe).toHaveBeenCalledWith(gauges[1], 20, { 'worker.id': 'worker-456' })
})
})
89 changes: 89 additions & 0 deletions packages/node/iii/tests/worker-metrics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { performance } from 'node:perf_hooks'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { WorkerMetricsCollector } from '../src/worker-metrics'

type MutableCollectorInternals = {
lastCpuUsage: NodeJS.CpuUsage
lastCpuTime: number
eventLoopHistogram: unknown
}

describe('WorkerMetricsCollector', () => {
beforeEach(() => {
vi.restoreAllMocks()
vi.clearAllMocks()
})

it('collects memory and cpu metrics and caps cpu percent at 100', () => {
const histogram = {
mean: 8_000_000,
reset: vi.fn(),
disable: vi.fn(),
}

vi.spyOn(Date, 'now').mockReturnValue(10_000)
vi.spyOn(process, 'cpuUsage').mockReturnValue({ user: 401_000, system: 101_000 })
vi.spyOn(process, 'memoryUsage').mockReturnValue({
rss: 1_024,
heapTotal: 2_048,
heapUsed: 1_536,
external: 256,
arrayBuffers: 128,
})
vi.spyOn(performance, 'now').mockReturnValue(1_500)

const collector = new WorkerMetricsCollector({ eventLoopResolutionMs: 0 })
const mutableCollector = collector as unknown as MutableCollectorInternals
mutableCollector.lastCpuUsage = { user: 1_000, system: 500 }
mutableCollector.lastCpuTime = 1_000
mutableCollector.eventLoopHistogram = histogram
const metrics = collector.collect()

expect(histogram.reset).toHaveBeenCalledOnce()
expect(metrics).toMatchObject({
memory_rss: 1_024,
memory_heap_total: 2_048,
memory_heap_used: 1_536,
memory_external: 256,
cpu_user_micros: 401_000,
cpu_system_micros: 101_000,
cpu_percent: 100,
event_loop_lag_ms: 8,
uptime_seconds: 0,
timestamp_ms: 10_000,
runtime: 'node',
})
})

it('stops monitoring and clears the histogram reference', () => {
const histogram = {
mean: 0,
reset: vi.fn(),
disable: vi.fn(),
}

vi.spyOn(Date, 'now').mockReturnValue(20_500)
vi.spyOn(process, 'cpuUsage').mockReturnValue({ user: 1_000, system: 500 })
vi.spyOn(process, 'memoryUsage').mockReturnValue({
rss: 2_048,
heapTotal: 4_096,
heapUsed: 3_072,
external: 512,
arrayBuffers: 128,
})
vi.spyOn(performance, 'now').mockReturnValue(600)

const collector = new WorkerMetricsCollector({ eventLoopResolutionMs: 5.8 })
const mutableCollector = collector as unknown as MutableCollectorInternals
mutableCollector.lastCpuUsage = { user: 500, system: 250 }
mutableCollector.lastCpuTime = 100
mutableCollector.eventLoopHistogram = histogram
const metrics = collector.collect()
collector.stopMonitoring()

expect(metrics.cpu_percent).toBeCloseTo(0.15)
expect(metrics.event_loop_lag_ms).toBe(0)
expect(histogram.disable).toHaveBeenCalledOnce()
expect(mutableCollector.eventLoopHistogram).toBeNull()
})
})
13 changes: 13 additions & 0 deletions packages/node/iii/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,18 @@ export default defineConfig({
testTimeout: 30000,
hookTimeout: 30000,
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
reporter: ['text', 'lcov'],
reportsDirectory: './coverage',
exclude: ['src/stream.ts', 'src/triggers.ts', 'src/types.ts'],
thresholds: {
lines: 70,
functions: 70,
branches: 70,
statements: 70,
},
},
},
})
Loading