diff --git a/.changeset/admin-metrics-collector-foundation.md b/.changeset/admin-metrics-collector-foundation.md new file mode 100644 index 00000000..4c2ecb60 --- /dev/null +++ b/.changeset/admin-metrics-collector-foundation.md @@ -0,0 +1,5 @@ +--- +"nostream": minor +--- + +feat: add OpenTelemetry metrics bootstrap with OTLP export for Prometheus diff --git a/.env.example b/.env.example index 78269d35..66c82e1d 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,11 @@ WORKER_COUNT=2 # Defaults to CPU count. Use 1 or 2 for local testing. # Useful namespaces: maintenance-worker, database-client:*, cache-client, etc. # DEBUG=maintenance-worker +# --- OBSERVABILITY (OpenTelemetry Metrics) --- +# OTEL_SERVICE_NAME=nostream +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 +# OTEL_METRIC_EXPORT_INTERVAL_MS=10000 + # --- RELAY PRIVATE KEY (Optional) --- # RELAY_PRIVATE_KEY=your_hex_private_key diff --git a/docker-compose.yml b/docker-compose.yml index df7c8d48..294cc173 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,10 @@ services: OPENNODE_API_KEY: ${OPENNODE_API_KEY} # Lnbits LNBITS_API_KEY: ${LNBITS_API_KEY} + # OpenTelemetry metrics + OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-nostream} + OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318} + OTEL_METRIC_EXPORT_INTERVAL_MS: ${OTEL_METRIC_EXPORT_INTERVAL_MS:-10000} # Enable DEBUG for troubleshooting. Examples: # DEBUG: "primary:*" # DEBUG: "worker:*" @@ -72,12 +76,14 @@ services: condition: service_healthy nostream-migrate: condition: service_completed_successfully + otel-collector: + condition: service_started restart: on-failure networks: default: ipv4_address: 10.10.10.2 - nostream-db: + nostream-db: image: postgres:15 container_name: nostream-db environment: @@ -88,9 +94,9 @@ services: - ${PWD}/.nostr/data:/var/lib/postgresql/data - ${PWD}/.nostr/db-logs:/var/log/postgresql - ${PWD}/postgresql.conf:/postgresql.conf - networks: - default: - ipv4_address: 10.10.10.3 + networks: + default: + ipv4_address: 10.10.10.3 command: postgres -c 'config_file=/postgresql.conf' restart: always healthcheck: @@ -100,15 +106,15 @@ services: retries: 5 start_period: 360s - nostream-cache: + nostream-cache: image: redis:7.0.5-alpine3.16 container_name: nostream-cache volumes: - cache:/data command: redis-server --loglevel warning --requirepass nostr_ts_relay - networks: - default: - ipv4_address: 10.10.10.4 + networks: + default: + ipv4_address: 10.10.10.4 restart: always healthcheck: test: [ "CMD", "redis-cli", "ping", "|", "grep", "PONG" ] @@ -140,6 +146,37 @@ services: networks: default: + otel-collector: + image: otel/opentelemetry-collector-contrib:0.105.0 + container_name: otel-collector + command: ['--config=/etc/otelcol-contrib/collector-config.yaml'] + volumes: + - ./otel-collector-config.yaml:/etc/otelcol-contrib/collector-config.yaml:ro + ports: + - 127.0.0.1:4318:4318 + - 127.0.0.1:8889:8889 + restart: always + networks: + default: + + prometheus: + image: prom/prometheus:v2.54.0 + container_name: prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yaml' + - '--storage.tsdb.path=/prometheus' + volumes: + - ./prometheus.yaml:/etc/prometheus/prometheus.yaml:ro + - prometheus-data:/prometheus + ports: + - 127.0.0.1:9090:9090 + depends_on: + otel-collector: + condition: service_started + restart: always + networks: + default: + networks: default: name: nostream @@ -150,3 +187,4 @@ networks: volumes: cache: + prometheus-data: diff --git a/otel-collector-config.yaml b/otel-collector-config.yaml new file mode 100644 index 00000000..cb9763aa --- /dev/null +++ b/otel-collector-config.yaml @@ -0,0 +1,19 @@ +receivers: + otlp: + protocols: + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: {} + +exporters: + prometheus: + endpoint: 0.0.0.0:8889 + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] diff --git a/package.json b/package.json index 7913784a..49c045bf 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,10 @@ "@clack/prompts": "^1.2.0", "@getalby/sdk": "^5.0.0", "@noble/secp256k1": "1.7.1", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-metrics-otlp-http": "^0.219.0", + "@opentelemetry/resources": "^2.8.0", + "@opentelemetry/sdk-metrics": "^2.8.0", "axios": "^1.16.0", "cac": "^7.0.0", "colorette": "^2.0.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6180c834..18a604bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,18 @@ importers: '@noble/secp256k1': specifier: 1.7.1 version: 1.7.1 + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 + '@opentelemetry/exporter-metrics-otlp-http': + specifier: ^0.219.0 + version: 0.219.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.8.0 + version: 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': + specifier: ^2.8.0 + version: 2.8.0(@opentelemetry/api@1.9.1) axios: specifier: ^1.16.0 version: 1.16.0 @@ -606,6 +618,66 @@ packages: resolution: {integrity: sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@opentelemetry/api-logs@0.219.0': + resolution: {integrity: sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.8.0': + resolution: {integrity: sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-metrics-otlp-http@0.219.0': + resolution: {integrity: sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.219.0': + resolution: {integrity: sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.219.0': + resolution: {integrity: sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.8.0': + resolution: {integrity: sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.219.0': + resolution: {integrity: sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.8.0': + resolution: {integrity: sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.8.0': + resolution: {integrity: sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.41.1': + resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} + engines: {node: '>=14'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -4104,6 +4176,71 @@ snapshots: '@npmcli/name-from-folder@2.0.0': {} + '@opentelemetry/api-logs@0.219.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/exporter-metrics-otlp-http@0.219.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.219.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.219.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.219.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.219.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.219.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.219.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.8.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/resources@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-logs@0.219.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.219.0 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/sdk-metrics@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.8.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.41.1 + + '@opentelemetry/semantic-conventions@1.41.1': {} + '@pkgjs/parseargs@0.11.0': {} '@pnpm/constants@7.1.1': {} diff --git a/prometheus.yaml b/prometheus.yaml new file mode 100644 index 00000000..0d185162 --- /dev/null +++ b/prometheus.yaml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: otel-collector + static_configs: + - targets: + - otel-collector:8889 diff --git a/src/app/app.ts b/src/app/app.ts index 9ac4bd94..8569493b 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -10,6 +10,7 @@ import packageJson from '../../package.json' import { Serializable } from 'child_process' import { Settings } from '../@types/settings' import { SettingsStatic } from '../utils/settings' +import { shutdownMetricsTelemetry } from '../telemetry/metrics' const logger = createLogger('app-primary') @@ -153,8 +154,10 @@ export class App implements IRunnable { private onExit() { logger.info('exiting') - this.close(() => { - this.process.exit(0) + void shutdownMetricsTelemetry().finally(() => { + this.close(() => { + this.process.exit(0) + }) }) } diff --git a/src/app/maintenance-worker.ts b/src/app/maintenance-worker.ts index a58f8055..d5296603 100644 --- a/src/app/maintenance-worker.ts +++ b/src/app/maintenance-worker.ts @@ -15,6 +15,7 @@ import { InvoiceStatus } from '../@types/invoice' import { isExpiredInvoice } from '../utils/invoice' import { Nip05Verification } from '../@types/nip05' import { Settings } from '../@types/settings' +import { shutdownMetricsTelemetry } from '../telemetry/metrics' const UPDATE_INVOICE_INTERVAL = 60000 const NIP05_REVERIFICATION_BATCH_SIZE = 50 @@ -235,8 +236,10 @@ export class MaintenanceWorker implements IRunnable { private onExit() { logger('exiting') - this.close(() => { - this.process.exit(0) + void shutdownMetricsTelemetry().finally(() => { + this.close(() => { + this.process.exit(0) + }) }) } diff --git a/src/app/static-mirroring-worker.ts b/src/app/static-mirroring-worker.ts index 4ec79cdc..d725e404 100644 --- a/src/app/static-mirroring-worker.ts +++ b/src/app/static-mirroring-worker.ts @@ -28,6 +28,7 @@ import { IRunnable } from '../@types/base' import { OutgoingEventMessage } from '../@types/messages' import { RelayedEvent } from '../@types/event' import { WebSocketServerAdapterEvent } from '../constants/adapter' +import { shutdownMetricsTelemetry } from '../telemetry/metrics' const logger = createLogger('static-mirror-worker') @@ -337,8 +338,10 @@ export class StaticMirroringWorker implements IRunnable { private onExit() { logger('exiting') - this.close(() => { - this.process.exit(0) + void shutdownMetricsTelemetry().finally(() => { + this.close(() => { + this.process.exit(0) + }) }) } diff --git a/src/app/worker.ts b/src/app/worker.ts index b6e93e38..6512104a 100644 --- a/src/app/worker.ts +++ b/src/app/worker.ts @@ -5,6 +5,7 @@ import { closeCacheClient } from '../cache/client' import { createLogger } from '../factories/logger-factory' import { FSWatcher } from 'fs' import { SettingsStatic } from '../utils/settings' +import { shutdownMetricsTelemetry } from '../telemetry/metrics' const logger = createLogger('app-worker') export class AppWorker implements IRunnable { @@ -46,8 +47,10 @@ export class AppWorker implements IRunnable { private onExit() { logger('exiting') - this.close(() => { - this.process.exit(0) + void shutdownMetricsTelemetry().finally(() => { + this.close(() => { + this.process.exit(0) + }) }) } diff --git a/src/index.ts b/src/index.ts index b7e3a64e..cdae9e1e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import cluster from 'cluster' import { appFactory } from './factories/app-factory' import { maintenanceWorkerFactory } from './factories/maintenance-worker-factory' import { staticMirroringWorkerFactory } from './factories/static-mirroring.worker-factory' +import { initializeMetricsTelemetry } from './telemetry/metrics' import { workerFactory } from './factories/worker-factory' export const getRunner = () => { @@ -23,5 +24,7 @@ export const getRunner = () => { } if (require.main === module) { + initializeMetricsTelemetry() + getRunner().run() } diff --git a/src/telemetry/metrics.ts b/src/telemetry/metrics.ts new file mode 100644 index 00000000..75d3f143 --- /dev/null +++ b/src/telemetry/metrics.ts @@ -0,0 +1,152 @@ +import cluster from 'cluster' +import os from 'os' + +import { metrics, type Counter, type UpDownCounter } from '@opentelemetry/api' +import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http' +import { resourceFromAttributes } from '@opentelemetry/resources' +import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics' + +import { createLogger } from '../factories/logger-factory' + +const logger = createLogger('telemetry-metrics') +const DEFAULT_EXPORT_INTERVAL_MS = 10000 + +export interface RelayMetricInstruments { + eventsAcceptedTotal: Counter + eventsRejectedTotal: Counter + websocketConnections: UpDownCounter +} + +let relayMetricInstruments: RelayMetricInstruments | undefined +let meterProvider: MeterProvider | undefined + +const getWorkerAttributes = () => { + const workerType = process.env.WORKER_TYPE ?? (cluster.isPrimary ? 'primary' : 'worker') + const workerIndex = process.env.WORKER_INDEX + + return { + worker_type: workerType, + ...(workerIndex ? { worker_index: workerIndex } : {}), + } +} + +const getCpuLoadPercent = (): number => { + const cores = Math.max(os.cpus().length, 1) + const oneMinuteLoad = os.loadavg()[0] + + return Number(((oneMinuteLoad / cores) * 100).toFixed(2)) +} + +const getMemoryUsedMb = (): number => { + return Number((process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)) +} + +const ensureExporterEndpoint = (): string | undefined => { + const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT + if (!endpoint) { + return undefined + } + + const normalized = endpoint.replace(/\/+$/, '') + if (normalized.endsWith('/v1/metrics')) { + return normalized + } + + return `${normalized}/v1/metrics` +} + +const getExportIntervalMillis = (): number => { + const intervalCandidate = Number(process.env.OTEL_METRIC_EXPORT_INTERVAL_MS || DEFAULT_EXPORT_INTERVAL_MS) + + if (!Number.isFinite(intervalCandidate) || intervalCandidate <= 0) { + logger.warn( + 'invalid OTEL_METRIC_EXPORT_INTERVAL_MS=%o, falling back to %d', + process.env.OTEL_METRIC_EXPORT_INTERVAL_MS, + DEFAULT_EXPORT_INTERVAL_MS, + ) + return DEFAULT_EXPORT_INTERVAL_MS + } + + return intervalCandidate +} + +const createRelayMetricInstruments = (): RelayMetricInstruments => { + const meter = metrics.getMeter('nostream') + + const workerAttributes = getWorkerAttributes() + const processCpuLoadGauge = meter.createObservableGauge('nostream.process.cpu_load_percent', { + description: 'CPU load percent normalized by number of CPU cores', + }) + const processMemoryGauge = meter.createObservableGauge('nostream.process.memory_used_mb', { + description: 'Heap memory used by current process in MB', + }) + + meter.addBatchObservableCallback( + (observableResult) => { + observableResult.observe(processCpuLoadGauge, getCpuLoadPercent(), workerAttributes) + observableResult.observe(processMemoryGauge, getMemoryUsedMb(), workerAttributes) + }, + [processCpuLoadGauge, processMemoryGauge], + ) + + return { + eventsAcceptedTotal: meter.createCounter('nostream.events.accepted_total', { + description: 'Total number of accepted events', + }), + eventsRejectedTotal: meter.createCounter('nostream.events.rejected_total', { + description: 'Total number of rejected events', + }), + websocketConnections: meter.createUpDownCounter('nostream.websocket.connections', { + description: 'Active websocket connections', + }), + } +} + +export const initializeMetricsTelemetry = (): RelayMetricInstruments => { + if (relayMetricInstruments) { + return relayMetricInstruments + } + + const endpoint = ensureExporterEndpoint() + if (!endpoint) { + logger('OTEL_EXPORTER_OTLP_ENDPOINT is not set; metrics exporter is disabled') + relayMetricInstruments = createRelayMetricInstruments() + return relayMetricInstruments + } + + meterProvider = new MeterProvider({ + resource: resourceFromAttributes({ + 'service.name': process.env.OTEL_SERVICE_NAME ?? 'nostream', + 'service.version': process.env.npm_package_version ?? 'unknown', + ...getWorkerAttributes(), + }), + readers: [ + new PeriodicExportingMetricReader({ + exporter: new OTLPMetricExporter({ url: endpoint }), + exportIntervalMillis: getExportIntervalMillis(), + }), + ], + }) + + metrics.setGlobalMeterProvider(meterProvider) + relayMetricInstruments = createRelayMetricInstruments() + logger('metrics exporter enabled; endpoint=%s', endpoint) + + return relayMetricInstruments +} + +export const getRelayMetricInstruments = (): RelayMetricInstruments => { + return relayMetricInstruments ?? initializeMetricsTelemetry() +} + +export const shutdownMetricsTelemetry = async (): Promise => { + if (!meterProvider) { + return + } + + try { + await meterProvider.shutdown() + } catch (error) { + logger.warn('error while shutting down metrics provider: %o', error) + } +} diff --git a/test/unit/app/maintenance-worker.spec.ts b/test/unit/app/maintenance-worker.spec.ts index 7c48bd29..d72bbae0 100644 --- a/test/unit/app/maintenance-worker.spec.ts +++ b/test/unit/app/maintenance-worker.spec.ts @@ -498,8 +498,9 @@ describe('MaintenanceWorker', () => { }) describe('onExit', () => { - it('calls close and then exits the process with code 0', () => { + it('calls close and then exits the process with code 0', async () => { fakeProcess.emit('SIGTERM') + await new Promise((resolve) => setImmediate(resolve)) expect(fakeProcess.exit).to.have.been.calledOnceWithExactly(0) })