From 85fda82889a098a357c162a2545cc59eaa9414ce Mon Sep 17 00:00:00 2001 From: Christian Galsterer Date: Wed, 8 May 2024 11:22:43 +0200 Subject: [PATCH] feat: support for multiple instances of exporter, multiple registrations of metrics --- src/mongoDBDriverExporter.ts | 95 +++++++++++++++++------------- test/metrics.spec.ts | 1 + test/mongoDBDriverExporter.spec.ts | 88 +++++++++++++++------------ 3 files changed, 104 insertions(+), 80 deletions(-) diff --git a/src/mongoDBDriverExporter.ts b/src/mongoDBDriverExporter.ts index ae972ba..728f977 100644 --- a/src/mongoDBDriverExporter.ts +++ b/src/mongoDBDriverExporter.ts @@ -19,8 +19,15 @@ export class MongoDBDriverExporter { private readonly checkedOut: Gauge private readonly waitQueueSize: Gauge + private readonly MONGODB_DRIVER_POOL_SIZE = 'mongodb_driver_pool_size' + private readonly MONGODB_DRIVER_POOL_MIN = 'mongodb_driver_pool_min' + private readonly MONGODB_DRIVER_POOL_MAX = 'mongodb_driver_pool_max' + private readonly MONGODB_DRIVER_POOL_CHECKEDOUT = 'mongodb_driver_pool_checkedout' + private readonly MONGODB_DRIVER_POOL_WAITQUEUESIZE = 'mongodb_driver_pool_waitqueuesize' + // command metrics private readonly commands: Histogram + private readonly MONGODB_DRIVER_COMMANDS_SECONDS = 'mongodb_driver_commands_seconds' constructor (mongoClient: MongoClient, register: Registry, options?: MongoDBDriverExporterOptions) { this.mongoClient = mongoClient @@ -29,49 +36,55 @@ export class MongoDBDriverExporter { const prefix = options?.prefix ?? '' - this.poolSize = new Gauge({ - name: `${prefix}mongodb_driver_pool_size`, - help: 'the current size of the connection pool, including idle and in-use members', - labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), - registers: [this.register] - }) - - this.minSize = new Gauge({ - name: `${prefix}mongodb_driver_pool_min`, - help: 'the minimum size of the connection pool', - labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), - registers: [this.register] - }) - - this.maxSize = new Gauge({ - name: `${prefix}mongodb_driver_pool_max`, - help: 'the maximum size of the connection pool', - labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), - registers: [this.register] - }) - - this.checkedOut = new Gauge({ - name: `${prefix}mongodb_driver_pool_checkedout`, - help: 'the count of connections that are currently in use', - labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), - registers: [this.register] - }) - - this.waitQueueSize = new Gauge({ - name: `${prefix}mongodb_driver_pool_waitqueuesize`, - help: 'the current size of the wait queue for a connection from the pool', - labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), - registers: [this.register] - }) + this.poolSize = (this.register.getSingleMetric(`${prefix}${this.MONGODB_DRIVER_POOL_SIZE}`) ?? + new Gauge({ + name: `${prefix}${this.MONGODB_DRIVER_POOL_SIZE}`, + help: 'the current size of the connection pool, including idle and in-use members', + labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), + registers: [this.register] + })) as Gauge - if (this.monitorCommands()) { - this.commands = new Histogram({ - name: `${prefix}mongodb_driver_commands_seconds`, - help: 'Timer of mongodb commands', - buckets: this.options.mongodbDriverCommandsSecondsHistogramBuckets, - labelNames: mergeLabelNamesWithStandardLabels(['command', 'server_address', 'status'], this.options.defaultLabels), + this.minSize = (this.register.getSingleMetric(`${prefix}${this.MONGODB_DRIVER_POOL_MIN}`) ?? + new Gauge({ + name: `${prefix}${this.MONGODB_DRIVER_POOL_MIN}`, + help: 'the minimum size of the connection pool', + labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), registers: [this.register] - }) + })) as Gauge + + this.maxSize = (this.register.getSingleMetric(`${prefix}${this.MONGODB_DRIVER_POOL_MAX}`) ?? + new Gauge({ + name: `${prefix}${this.MONGODB_DRIVER_POOL_MAX}`, + help: 'the maximum size of the connection pool', + labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), + registers: [this.register] + })) as Gauge + + this.checkedOut = (this.register.getSingleMetric(`${prefix}${this.MONGODB_DRIVER_POOL_CHECKEDOUT}`) ?? + new Gauge({ + name: `${prefix}${this.MONGODB_DRIVER_POOL_CHECKEDOUT}`, + help: 'the count of connections that are currently in use', + labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), + registers: [this.register] + })) as Gauge + + this.waitQueueSize = (this.register.getSingleMetric(`${prefix}${this.MONGODB_DRIVER_POOL_WAITQUEUESIZE}`) ?? + new Gauge({ + name: `${prefix}${this.MONGODB_DRIVER_POOL_WAITQUEUESIZE}`, + help: 'the current size of the wait queue for a connection from the pool', + labelNames: mergeLabelNamesWithStandardLabels(['server_address'], this.options.defaultLabels), + registers: [this.register] + })) as Gauge + + if (this.monitorCommands()) { + this.commands = (this.register.getSingleMetric(`${prefix}${this.MONGODB_DRIVER_COMMANDS_SECONDS}`) ?? + new Histogram({ + name: `${prefix}${this.MONGODB_DRIVER_COMMANDS_SECONDS}`, + help: 'Timer of mongodb commands', + buckets: this.options.mongodbDriverCommandsSecondsHistogramBuckets, + labelNames: mergeLabelNamesWithStandardLabels(['command', 'server_address', 'status'], this.options.defaultLabels), + registers: [this.register] + })) as Histogram } } diff --git a/test/metrics.spec.ts b/test/metrics.spec.ts index ce57f0e..d293edb 100644 --- a/test/metrics.spec.ts +++ b/test/metrics.spec.ts @@ -28,6 +28,7 @@ describe('all metrics are created with the correct parameters', () => { beforeEach(() => { jest.clearAllMocks() + register.getSingleMetric = jest.fn(() => undefined) }) test('all metrics are created', () => { diff --git a/test/mongoDBDriverExporter.spec.ts b/test/mongoDBDriverExporter.spec.ts index 67ac221..b346b33 100644 --- a/test/mongoDBDriverExporter.spec.ts +++ b/test/mongoDBDriverExporter.spec.ts @@ -4,6 +4,28 @@ import { Registry } from 'prom-client' import { MongoDBDriverExporter } from '../src/mongoDBDriverExporter' +const poolMetrics: string[] = [ + 'mongodb_driver_pool_size', 'mongodb_driver_pool_min', 'mongodb_driver_pool_max', + 'mongodb_driver_pool_checkedout', 'mongodb_driver_pool_waitqueuesize' +] + +const commandMetrics: string[] = [ + 'mongodb_driver_commands_seconds' +] + +const allMetrics: string[] = poolMetrics.concat(commandMetrics) + +const connectionEvents: string[] = [ + 'connectionPoolCreated', 'connectionPoolClosed', 'connectionCreated', 'connectionClosed', 'connectionCheckOutStarted', + 'connectionCheckedOut', 'connectionCheckOutFailed', 'connectionCheckedIn' +] + +const commandEvents: string[] = [ + 'commandSucceeded', 'commandFailed' +] + +const allEvents: string[] = connectionEvents.concat(commandEvents) + describe('tests mongoDBDriverExporter with real mongo client', () => { let register: Registry @@ -25,10 +47,6 @@ describe('tests mongoDBDriverExporter with real mongo client', () => { }) test('connection and commands metrics are registered in registry with optional configurations', () => { - const metrics: string[] = [ - 'mongodb_driver_pool_size', 'mongodb_driver_pool_min', 'mongodb_driver_pool_max', - 'mongodb_driver_pool_checkedout', 'mongodb_driver_pool_waitqueuesize', 'mongodb_driver_commands_seconds' - ] const mongoClient = new MongoClient('mongodb://localhost:27017', { monitorCommands: true }) const options = { mongodbDriverCommandsSecondsHistogramBuckets: [0.001, 0.005, 0.010, 0.020, 0.030, 0.040, 0.050, 0.100, 0.200, 0.500, 1.0, 2.0, 5.0, 20], @@ -36,57 +54,58 @@ describe('tests mongoDBDriverExporter with real mongo client', () => { } // eslint-disable-next-line no-new new MongoDBDriverExporter(mongoClient, register, options) - expect(register.getMetricsAsArray()).toHaveLength(metrics.length) - metrics.forEach(metric => { + expect(register.getMetricsAsArray()).toHaveLength(allMetrics.length) + allMetrics.forEach(metric => { expect(register.getSingleMetric(metric)).toBeDefined() }) }) test('only connection metrics are registered in registry', () => { - const metrics: string[] = [ - 'mongodb_driver_pool_size', 'mongodb_driver_pool_min', 'mongodb_driver_pool_max', - 'mongodb_driver_pool_checkedout', 'mongodb_driver_pool_waitqueuesize' - ] const mongoClient = new MongoClient('mongodb://localhost:27017', { monitorCommands: false }) const exporter = new MongoDBDriverExporter(mongoClient, register) exporter.enableMetrics() - expect(register.getMetricsAsArray()).toHaveLength(metrics.length) - metrics.forEach(metric => { + expect(register.getMetricsAsArray()).toHaveLength(poolMetrics.length) + poolMetrics.forEach(metric => { expect(register.getSingleMetric(metric)).toBeDefined() }) }) test('event connection and command listeners are registered for mongo client events', () => { - const events: string[] = [ - 'connectionPoolCreated', 'connectionPoolClosed', 'connectionCreated', 'connectionClosed', 'connectionCheckOutStarted', - 'connectionCheckedOut', 'connectionCheckOutFailed', 'connectionCheckedIn', 'commandSucceeded', 'commandFailed' - ] const mongoClient = new MongoClient('mongodb://localhost:27017', { monitorCommands: true }) const exporter = new MongoDBDriverExporter(mongoClient, register) exporter.enableMetrics() - expect(mongoClient.eventNames()).toHaveLength(events.length) - expect(mongoClient.eventNames()).toEqual(expect.arrayContaining(events)) - events.forEach(event => { + expect(mongoClient.eventNames()).toHaveLength(allEvents.length) + expect(mongoClient.eventNames()).toEqual(expect.arrayContaining(allEvents)) + allEvents.forEach(event => { expect(mongoClient.listeners(event)).toHaveLength(1) expect(mongoClient.listeners(event).at(0)).toBeInstanceOf(Function) }) }) test('only event connection listeners are registered for mongo client events', () => { - const events: string[] = [ - 'connectionPoolCreated', 'connectionPoolClosed', 'connectionCreated', 'connectionClosed', 'connectionCheckOutStarted', - 'connectionCheckedOut', 'connectionCheckOutFailed', 'connectionCheckedIn' - ] const mongoClient = new MongoClient('mongodb://localhost:27017', { monitorCommands: false }) const exporter = new MongoDBDriverExporter(mongoClient, register) exporter.enableMetrics() - expect(mongoClient.eventNames()).toHaveLength(events.length) - expect(mongoClient.eventNames()).toEqual(expect.arrayContaining(events)) - events.forEach(event => { + expect(mongoClient.eventNames()).toHaveLength(connectionEvents.length) + expect(mongoClient.eventNames()).toEqual(expect.arrayContaining(connectionEvents)) + connectionEvents.forEach(event => { expect(mongoClient.listeners(event)).toHaveLength(1) expect(mongoClient.listeners(event).at(0)).toBeInstanceOf(Function) }) }) + + test('registered metrics are taken from the registry', () => { + const mongoClient = new MongoClient('mongodb://localhost:27017', { monitorCommands: true }) + // eslint-disable-next-line no-new + new MongoDBDriverExporter(mongoClient, register) + // eslint-disable-next-line no-new + new MongoDBDriverExporter(mongoClient, register) + + expect(register.getMetricsAsArray()).toHaveLength(allMetrics.length) + allMetrics.forEach((metric) => { + expect(register.getSingleMetric(metric)).toBeDefined() + }) + }) }) describe('enableMetrics attach event listeners', () => { @@ -114,15 +133,10 @@ describe('enableMetrics attach event listeners', () => { } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument exporter = new MongoDBDriverExporter(mockMongoClient, register, mockOptions) - const events: string[] = [ - 'connectionPoolCreated', 'connectionPoolClosed', 'connectionCreated', 'connectionClosed', 'connectionCheckOutStarted', - 'connectionCheckedOut', 'connectionCheckOutFailed', 'connectionCheckedIn' - ] exporter.enableMetrics() - - events.forEach(event => { + connectionEvents.forEach(event => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockMongoClient.on).toHaveBeenCalledTimes(events.length) + expect(mockMongoClient.on).toHaveBeenCalledTimes(connectionEvents.length) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockMongoClient.on).toHaveBeenCalledWith(event, expect.any(Function)) }) @@ -140,15 +154,11 @@ describe('enableMetrics attach event listeners', () => { } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument exporter = new MongoDBDriverExporter(mockMongoClient, register, mockOptions) - const events: string[] = [ - 'connectionPoolCreated', 'connectionPoolClosed', 'connectionCreated', 'connectionClosed', 'connectionCheckOutStarted', - 'connectionCheckedOut', 'connectionCheckOutFailed', 'connectionCheckedIn', 'commandSucceeded', 'commandFailed' - ] exporter.enableMetrics() - events.forEach(event => { + allEvents.forEach(event => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(mockMongoClient.on).toHaveBeenCalledTimes(events.length) + expect(mockMongoClient.on).toHaveBeenCalledTimes(allEvents.length) // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(mockMongoClient.on).toHaveBeenCalledWith(event, expect.any(Function)) })