Skip to content

Commit

Permalink
feat: support for multiple instances of exporter, multiple registrati…
Browse files Browse the repository at this point in the history
…ons of metrics
  • Loading branch information
christiangalsterer committed May 8, 2024
1 parent 45fef5c commit 85fda82
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 80 deletions.
95 changes: 54 additions & 41 deletions src/mongoDBDriverExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}

Expand Down
1 change: 1 addition & 0 deletions test/metrics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
88 changes: 49 additions & 39 deletions test/mongoDBDriverExporter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -25,68 +47,65 @@ 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],
defaultLabels: { foo: 'bar', alice: 2 }
}
// 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', () => {
Expand Down Expand Up @@ -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))
})
Expand All @@ -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))
})
Expand Down

0 comments on commit 85fda82

Please sign in to comment.