Skip to content

Commit 5f1a5bf

Browse files
chore(logs): improve mongo and graphql logs (#555)
* chore(logs): improve mongo and graphql logs * beautify logs * Bump version up to 1.2.5 * rm utils * improve grapgql log * improve mongo logs * Update logger.ts --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 323e42f commit 5f1a5bf

File tree

8 files changed

+244
-47
lines changed

8 files changed

+244
-47
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.2.4",
3+
"version": "1.2.5",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

src/index.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import PlansFactory from './models/plansFactory';
2626
import BusinessOperationsFactory from './models/businessOperationsFactory';
2727
import schema from './schema';
2828
import { graphqlUploadExpress } from 'graphql-upload';
29-
import morgan from 'morgan';
3029
import { metricsMiddleware, createMetricsServer, graphqlMetricsPlugin } from './metrics';
30+
import { requestLogger } from './utils/logger';
3131

3232
/**
3333
* Option to enable playground
@@ -85,19 +85,17 @@ class HawkAPI {
8585
next();
8686
});
8787

88-
/**
89-
* Setup request logger.
90-
* Uses 'combined' format in production for Apache-style logging,
91-
* and 'dev' format in development for colored, concise output.
92-
*/
93-
this.app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
94-
9588
/**
9689
* Add metrics middleware to track HTTP requests
9790
*/
9891
this.app.use(metricsMiddleware);
9992

10093
this.app.use(express.json());
94+
95+
/**
96+
* Setup request logger with custom formatters (GraphQL operation name support)
97+
*/
98+
this.app.use(requestLogger);
10199
this.app.use(bodyParser.urlencoded({ extended: false }));
102100
this.app.use('/static', express.static(`./static`));
103101

src/metrics/mongodb.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import promClient from 'prom-client';
22
import { MongoClient, MongoClientOptions } from 'mongodb';
3+
import { Effect, sgr } from '../utils/ansi';
34

45
/**
56
* MongoDB command duration histogram
@@ -113,12 +114,155 @@ export function withMongoMetrics(options: MongoClientOptions = {}): MongoClientO
113114
};
114115
}
115116

117+
/**
118+
* Format filter/update parameters for logging
119+
* @param params - Parameters to format
120+
* @returns Formatted string
121+
*/
122+
function formatParams(params: any): string {
123+
if (!params || Object.keys(params).length === 0) {
124+
return '';
125+
}
126+
127+
try {
128+
return JSON.stringify(params);
129+
} catch (e) {
130+
return String(params);
131+
}
132+
}
133+
134+
/**
135+
* Colorize duration based on performance thresholds
136+
* @param duration - Duration in milliseconds
137+
* @returns Colorized duration string
138+
*/
139+
function colorizeDuration(duration: number): string {
140+
let color: Effect;
141+
142+
if (duration < 50) {
143+
color = Effect.ForegroundGreen;
144+
} else if (duration < 100) {
145+
color = Effect.ForegroundYellow;
146+
} else {
147+
color = Effect.ForegroundRed;
148+
}
149+
150+
return sgr(`${duration}ms`, color);
151+
}
152+
153+
/**
154+
* Interface for storing command information with timestamp
155+
*/
156+
interface StoredCommandInfo {
157+
formattedCommand: string;
158+
timestamp: number;
159+
}
160+
161+
/**
162+
* Map to store formatted command information by requestId
163+
*/
164+
const commandInfoMap = new Map<number, StoredCommandInfo>();
165+
166+
/**
167+
* Timeout for cleaning up stale command info (30 seconds)
168+
*/
169+
const COMMAND_INFO_TIMEOUT_MS = 30000;
170+
171+
/**
172+
* Cleanup stale command info to prevent memory leaks
173+
* Removes entries older than COMMAND_INFO_TIMEOUT_MS
174+
*/
175+
function cleanupStaleCommandInfo(): void {
176+
const now = Date.now();
177+
const keysToDelete: number[] = [];
178+
179+
for (const [requestId, info] of commandInfoMap.entries()) {
180+
if (now - info.timestamp > COMMAND_INFO_TIMEOUT_MS) {
181+
keysToDelete.push(requestId);
182+
}
183+
}
184+
185+
if (keysToDelete.length > 0) {
186+
console.warn(`Cleaning up ${keysToDelete.length} stale MongoDB command info entries (possible memory leak)`);
187+
for (const key of keysToDelete) {
188+
commandInfoMap.delete(key);
189+
}
190+
}
191+
}
192+
193+
/**
194+
* Periodic cleanup interval
195+
*/
196+
setInterval(cleanupStaleCommandInfo, COMMAND_INFO_TIMEOUT_MS);
197+
198+
/**
199+
* Store MongoDB command details for later logging
200+
* @param event - MongoDB command event
201+
*/
202+
function storeCommandInfo(event: any): void {
203+
const collectionRaw = extractCollectionFromCommand(event.command, event.commandName);
204+
const collection = sgr(normalizeCollectionName(collectionRaw), Effect.ForegroundGreen);
205+
const db = event.databaseName || 'unknown db';
206+
const commandName = sgr(event.commandName, Effect.ForegroundRed);
207+
const filter = event.command.filter;
208+
const update = event.command.update;
209+
const pipeline = event.command.pipeline;
210+
const projection = event.command.projection;
211+
const params = filter || update || pipeline;
212+
const paramsStr = formatParams(params);
213+
const projectionStr = projection ? ` projection: ${formatParams(projection)}` : '';
214+
215+
const formattedCommand = `[${event.requestId}] ${db}.${collection}.${commandName}(${paramsStr})${projectionStr}`;
216+
217+
commandInfoMap.set(event.requestId, {
218+
formattedCommand,
219+
timestamp: Date.now(),
220+
});
221+
}
222+
223+
/**
224+
* Log MongoDB command success to console
225+
* Format: [requestId] db.collection.command(params) ✓ duration
226+
* @param event - MongoDB command event
227+
*/
228+
function logCommandSucceeded(event: any): void {
229+
const info = commandInfoMap.get(event.requestId);
230+
const durationStr = colorizeDuration(event.duration);
231+
232+
if (info) {
233+
console.log(`${info.formattedCommand}${durationStr}`);
234+
commandInfoMap.delete(event.requestId);
235+
} else {
236+
console.log(`[${event.requestId}] ${event.commandName}${durationStr}`);
237+
}
238+
}
239+
240+
/**
241+
* Log MongoDB command failure to console
242+
* Format: [requestId] db.collection.command(params) ✗ error duration
243+
* @param event - MongoDB command event
244+
*/
245+
function logCommandFailed(event: any): void {
246+
const errorMsg = event.failure?.message || event.failure?.errmsg || 'Unknown error';
247+
const info = commandInfoMap.get(event.requestId);
248+
const durationStr = colorizeDuration(event.duration);
249+
250+
if (info) {
251+
console.error(`${info.formattedCommand}${errorMsg} ${durationStr}`);
252+
commandInfoMap.delete(event.requestId);
253+
} else {
254+
console.error(`[${event.requestId}] ${event.commandName}${errorMsg} ${durationStr}`);
255+
}
256+
}
257+
116258
/**
117259
* Setup MongoDB metrics monitoring on a MongoClient
118260
* @param client - MongoDB client to monitor
119261
*/
120262
export function setupMongoMetrics(client: MongoClient): void {
121263
client.on('commandStarted', (event) => {
264+
storeCommandInfo(event);
265+
122266
// Store start time and metadata for this command
123267
const metadataKey = `${event.requestId}`;
124268

@@ -139,6 +283,8 @@ export function setupMongoMetrics(client: MongoClient): void {
139283
});
140284

141285
client.on('commandSucceeded', (event) => {
286+
logCommandSucceeded(event);
287+
142288
const metadataKey = `${event.requestId}`;
143289
// eslint-disable-next-line @typescript-eslint/no-explicit-any
144290
const metadata = (client as any)[metadataKey];
@@ -157,6 +303,8 @@ export function setupMongoMetrics(client: MongoClient): void {
157303
});
158304

159305
client.on('commandFailed', (event) => {
306+
logCommandFailed(event);
307+
160308
const metadataKey = `${event.requestId}`;
161309
// eslint-disable-next-line @typescript-eslint/no-explicit-any
162310
const metadata = (client as any)[metadataKey];

src/models/eventsFactory.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ class EventsFactory extends Factory {
665665
async getEventRelease(eventId) {
666666
const eventOriginal = await this.findById(eventId);
667667

668-
if (!eventOriginal) {
668+
if (!eventOriginal || !eventOriginal.payload.release) {
669669
return null;
670670
}
671671

src/mongo.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ export async function setupConnections(): Promise<void> {
7575
databases.hawk = hawkMongoClient.db();
7676
databases.events = eventsMongoClient.db();
7777

78-
// Setup metrics monitoring for both clients
78+
/**
79+
* Log and and measure MongoDB metrics
80+
*/
7981
setupMongoMetrics(hawkMongoClient);
8082
setupMongoMetrics(eventsMongoClient);
8183
} catch (e) {

src/utils/ansi.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* ANSI escape codes for text formatting
3+
*/
4+
export enum Effect {
5+
Reset = '\x1b[0m',
6+
Bold = '\x1b[1m',
7+
Underline = '\x1b[4m',
8+
CrossedOut = '\x1b[9m',
9+
BoldOff = '\x1b[22m',
10+
UnderlineOff = '\x1b[24m',
11+
CrossedOutOff = '\x1b[29m',
12+
ForegroundRed = '\x1b[31m',
13+
ForegroundGreen = '\x1b[32m',
14+
ForegroundYellow = '\x1b[33m',
15+
ForegroundBlue = '\x1b[34m',
16+
ForegroundMagenta = '\x1b[35m',
17+
ForegroundCyan = '\x1b[36m',
18+
ForegroundWhite = '\x1b[37m',
19+
ForegroundGray = '\x1b[90m',
20+
BackgroundRed = '\x1b[41m',
21+
BackgroundGreen = '\x1b[42m',
22+
BackgroundYellow = '\x1b[43m',
23+
BackgroundBlue = '\x1b[44m',
24+
BackgroundMagenta = '\x1b[45m',
25+
BackgroundCyan = '\x1b[46m',
26+
BackgroundWhite = '\x1b[47m',
27+
BackgroundGray = '\x1b[100m',
28+
};
29+
30+
/**
31+
* ANSI escape code for setting visual effects using SGR (Select Graphic Rendition) subset
32+
*
33+
* @example console.log('Hello, ${sgr('world', Effect.ForegroundRed)}');
34+
*
35+
*
36+
* @param message - The message to colorize
37+
* @param color - The color to apply
38+
* @returns The colored message
39+
*/
40+
export function sgr(message: string, color: Effect | Effect[]): string {
41+
const colorCode = Array.isArray(color) ? color.join('') : color ?? Effect.Reset;
42+
43+
return `${colorCode}${message}${Effect.Reset}`;
44+
}

src/utils/logger.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import morgan from 'morgan';
2+
import express from 'express';
3+
import { sgr, Effect } from './ansi';
4+
5+
/**
6+
* Setup custom GraphQL-aware morgan token.
7+
* Extracts operation name from GraphQL requests to show query/mutation names in logs.
8+
*/
9+
morgan.token('graphql-operation', (req: express.Request) => {
10+
if (req.body && req.body.operationName) {
11+
return req.body.operationName;
12+
}
13+
if (req.body && req.body.query) {
14+
/* Try to extract operation name from query string if operationName is not provided */
15+
const match = req.body.query.match(/(?:query|mutation)\s+(\w+)/);
16+
const isMutation = req.body.query.includes('mutation');
17+
18+
const effect = isMutation ? Effect.ForegroundRed : Effect.ForegroundMagenta;
19+
const prefix = sgr(isMutation ? 'mutation' : 'query', effect);
20+
21+
if (match && match[1]) {
22+
return prefix + ' ' + sgr(sgr(match[1], effect), Effect.Bold);
23+
}
24+
}
25+
26+
return '-';
27+
});
28+
29+
/**
30+
* Custom morgan format for GraphQL-aware logging.
31+
* Development: shows method, url, operation name, status, response time, content length
32+
* Production: Apache-style format with operation name included
33+
*/
34+
const customFormat = process.env.NODE_ENV === 'production'
35+
? ':remote-addr - :remote-user [:date[clf]] ":method :url :graphql-operation" :status :res[content-length] bytes - :response-time ms'
36+
: ':method :url :graphql-operation :status :res[content-length] bytes - :response-time ms';
37+
38+
/**
39+
* Configured morgan middleware with GraphQL operation name logging
40+
*/
41+
export const requestLogger = morgan(customFormat);

src/utils/utils.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)