1- import client from 'prom-client' ;
1+ import promClient from 'prom-client' ;
22import { MongoClient , MongoClientOptions } from 'mongodb' ;
33
44/**
55 * MongoDB command duration histogram
6- * Tracks MongoDB command duration by command, collection, and database
6+ * Tracks MongoDB command duration by command, collection family , and database
77 */
8- export const mongoCommandDuration = new client . Histogram ( {
8+ export const mongoCommandDuration = new promClient . Histogram ( {
99 name : 'hawk_mongo_command_duration_seconds' ,
10- help : 'Histogram of MongoDB command duration by command, collection, and db' ,
11- labelNames : [ 'command' , 'collection ' , 'db' ] ,
12- buckets : [ 0.001 , 0.005 , 0. 01, 0.05 , 0.1 , 0.5 , 1 , 5 , 10 ] ,
10+ help : 'Histogram of MongoDB command duration by command, collection family , and db' ,
11+ labelNames : [ 'command' , 'collection_family ' , 'db' ] ,
12+ buckets : [ 0.01 , 0.05 , 0.1 , 0.5 , 1 , 5 , 10 ] ,
1313} ) ;
1414
1515/**
1616 * MongoDB command errors counter
1717 * Tracks failed MongoDB commands grouped by command and error code
1818 */
19- export const mongoCommandErrors = new client . Counter ( {
19+ export const mongoCommandErrors = new promClient . Counter ( {
2020 name : 'hawk_mongo_command_errors_total' ,
2121 help : 'Counter of failed MongoDB commands grouped by command and error code' ,
2222 labelNames : [ 'command' , 'error_code' ] ,
2323} ) ;
2424
25+ /**
26+ * Extract collection name from MongoDB command
27+ * Handles different command types and their collection name locations
28+ * @param command - MongoDB command object
29+ * @param commandName - Name of the command (find, insert, getMore, etc.)
30+ * @returns Raw collection identifier or null
31+ */
32+ function extractCollectionFromCommand ( command : any , commandName : string ) : unknown {
33+ if ( ! command ) {
34+ return null ;
35+ }
36+
37+ // Special handling for getMore command - collection is in a different field
38+ if ( commandName === 'getMore' ) {
39+ return command . collection || null ;
40+ }
41+
42+ /*
43+ * For most commands, collection name is the value of the command name key
44+ * e.g., { find: "users" } -> collection is "users"
45+ */
46+ return command [ commandName ] || null ;
47+ }
48+
49+ /**
50+ * Normalize collection value to string
51+ * Handles BSON types and other non-string values
52+ * @param collection - Collection value from MongoDB command
53+ * @returns Normalized string or 'unknown'
54+ */
55+ function normalizeCollectionName ( collection : unknown ) : string {
56+ if ( ! collection ) {
57+ return 'unknown' ;
58+ }
59+
60+ // Handle string values directly
61+ if ( typeof collection === 'string' ) {
62+ return collection ;
63+ }
64+
65+ // Handle BSON types and objects with toString method
66+ if ( typeof collection === 'object' && 'toString' in collection ) {
67+ try {
68+ const str = String ( collection ) ;
69+
70+ // Skip if toString returns object representation like [object Object]
71+ if ( ! str . startsWith ( '[object' ) && str !== 'unknown' ) {
72+ return str ;
73+ }
74+ } catch ( e ) {
75+ console . error ( 'Error normalizing collection name' , e ) ;
76+ // Ignore conversion errors
77+ }
78+ }
79+
80+ return 'unknown' ;
81+ }
82+
83+ /**
84+ * Extract collection family from full collection name
85+ * Reduces cardinality by grouping dynamic collections
86+ * @param collectionName - Full collection name (e.g., "events:projectId")
87+ * @returns Collection family (e.g., "events")
88+ */
89+ function getCollectionFamily ( collectionName : string ) : string {
90+ if ( collectionName === 'unknown' ) {
91+ return 'unknown' ;
92+ }
93+
94+ // Extract prefix before colon for dynamic collections
95+ const colonIndex = collectionName . indexOf ( ':' ) ;
96+
97+ if ( colonIndex > 0 ) {
98+ return collectionName . substring ( 0 , colonIndex ) ;
99+ }
100+
101+ return collectionName ;
102+ }
103+
25104/**
26105 * Enhance MongoClient options with monitoring
27106 * @param options - Original MongoDB connection options
@@ -40,71 +119,65 @@ export function withMongoMetrics(options: MongoClientOptions = {}): MongoClientO
40119 */
41120export function setupMongoMetrics ( client : MongoClient ) : void {
42121 client . on ( 'commandStarted' , ( event ) => {
43- // Store start time for this command
44- const startTimeKey = `${ event . requestId } ` ;
122+ // Store start time and metadata for this command
123+ const metadataKey = `${ event . requestId } ` ;
124+
125+ // Extract collection name from the command
126+ const collectionRaw = extractCollectionFromCommand ( event . command , event . commandName ) ;
127+ const collection = normalizeCollectionName ( collectionRaw ) ;
128+ const collectionFamily = getCollectionFamily ( collection ) ;
129+
130+ const db = event . databaseName || 'unknown' ;
45131
46132 // eslint-disable-next-line @typescript-eslint/no-explicit-any
47- ( client as any ) [ startTimeKey ] = Date . now ( ) ;
133+ ( client as any ) [ metadataKey ] = {
134+ startTime : Date . now ( ) ,
135+ collectionFamily,
136+ db,
137+ commandName : event . commandName ,
138+ } ;
48139 } ) ;
49140
50141 client . on ( 'commandSucceeded' , ( event ) => {
51- const startTimeKey = `${ event . requestId } ` ;
142+ const metadataKey = `${ event . requestId } ` ;
52143 // eslint-disable-next-line @typescript-eslint/no-explicit-any
53- const startTime = ( client as any ) [ startTimeKey ] ;
54-
55- if ( startTime ) {
56- const duration = ( Date . now ( ) - startTime ) / 1000 ;
144+ const metadata = ( client as any ) [ metadataKey ] ;
57145
58- /**
59- * Extract collection name from the command
60- * For most commands, the collection name is the value of the command name key
61- * e.g., { find: "users" } -> collection is "users"
62- */
63- // eslint-disable-next-line @typescript-eslint/no-explicit-any
64- const collection = event . command ? ( ( event . command as any ) [ event . commandName ] || 'unknown' ) : 'unknown' ;
65- const db = event . databaseName || 'unknown' ;
146+ if ( metadata ) {
147+ const duration = ( Date . now ( ) - metadata . startTime ) / 1000 ;
66148
67149 mongoCommandDuration
68- . labels ( event . commandName , collection , db )
150+ . labels ( metadata . commandName , metadata . collectionFamily , metadata . db )
69151 . observe ( duration ) ;
70152
71- // Clean up start time
153+ // Clean up metadata
72154 // eslint-disable-next-line @typescript-eslint/no-explicit-any
73- delete ( client as any ) [ startTimeKey ] ;
155+ delete ( client as any ) [ metadataKey ] ;
74156 }
75157 } ) ;
76158
77159 client . on ( 'commandFailed' , ( event ) => {
78- const startTimeKey = `${ event . requestId } ` ;
160+ const metadataKey = `${ event . requestId } ` ;
79161 // eslint-disable-next-line @typescript-eslint/no-explicit-any
80- const startTime = ( client as any ) [ startTimeKey ] ;
162+ const metadata = ( client as any ) [ metadataKey ] ;
81163
82- if ( startTime ) {
83- const duration = ( Date . now ( ) - startTime ) / 1000 ;
84-
85- /**
86- * Extract collection name from the command
87- * For most commands, the collection name is the value of the command name key
88- * e.g., { find: "users" } -> collection is "users"
89- */
90- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91- const collection = event . command ? ( ( event . command as any ) [ event . commandName ] || 'unknown' ) : 'unknown' ;
92- const db = event . databaseName || 'unknown' ;
164+ if ( metadata ) {
165+ const duration = ( Date . now ( ) - metadata . startTime ) / 1000 ;
93166
94167 mongoCommandDuration
95- . labels ( event . commandName , collection , db )
168+ . labels ( metadata . commandName , metadata . collectionFamily , metadata . db )
96169 . observe ( duration ) ;
97170
98171 // Track error
99172 const errorCode = event . failure ?. code ?. toString ( ) || 'unknown' ;
100173
101174 mongoCommandErrors
102- . labels ( event . commandName , errorCode )
175+ . labels ( metadata . commandName , errorCode )
103176 . inc ( ) ;
104177
105- // Clean up start time
178+ // Clean up metadata
106179 // eslint-disable-next-line @typescript-eslint/no-explicit-any
107- delete ( client as any ) [ startTimeKey ] ;
180+ delete ( client as any ) [ metadataKey ] ;
108181 }
109182 } ) ;
110183}
0 commit comments