Skip to content

Commit 92e6958

Browse files
authored
Merge pull request #205 from internxt/feat/add-better-observability
feat: add requestId and clientId to logs
2 parents 7f5aa8c + a98c26f commit 92e6958

5 files changed

Lines changed: 100 additions & 42 deletions

File tree

lib/core/buckets/usecase.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios, { AxiosError } from 'axios';
22
import { v4 } from 'uuid';
33
import lodash from 'lodash';
4+
import log from '../../logger';
45

56
import { Bucket } from './Bucket';
67
import { Frame } from '../frames/Frame';
@@ -240,7 +241,7 @@ export class BucketsUsecase {
240241
});
241242

242243
if (!bucketEntry) {
243-
console.warn(`[getFileInfo] Bucket entry not found in DB - bucketId: ${bucketId}, fileId: ${fileId}`);
244+
log.warn(`[getFileInfo] Bucket entry not found in DB - bucketId: ${bucketId}, fileId: ${fileId}`);
244245
throw new BucketEntryNotFoundError(fileId);
245246
}
246247

@@ -253,7 +254,7 @@ export class BucketsUsecase {
253254
if (axios.isAxiosError(error)) {
254255
const axiosError = error as AxiosError;
255256
if (axiosError.response?.status === 404) {
256-
console.warn(`[getFileInfo] Download links returned 404 - bucketEntryId: ${bucketEntry.id}, fileId: ${fileId}, url: ${axiosError.config?.url}`);
257+
log.warn(`[getFileInfo] Download links returned 404 - bucketEntryId: ${bucketEntry.id}, fileId: ${fileId}, url: ${axiosError.config?.url}`);
257258
throw new BucketEntryNotFoundError(fileId);
258259
}
259260
}
@@ -597,7 +598,7 @@ export class BucketsUsecase {
597598
.deleteManyByUuids(uploads.map((u) => u.uuid))
598599
.catch((err) => {
599600
// TODO: Move to EventBus
600-
console.log(
601+
log.info(
601602
'completeUpload/uploads-deletion: Failed due to %s. %s',
602603
err.message,
603604
err.stack
@@ -680,21 +681,23 @@ export class BucketsUsecase {
680681
} else {
681682
await this.validateObjectInStorage(contact, uuid, data_size).catch((error) => {
682683
if (error instanceof UploadSizeDoesNotMatchError) {
683-
console.error(`[finishUpload][SizeDoesNotMatchError] Error size match ${JSON.stringify({ uuid, expectedSize: data_size, contactId: contact.id, message: error.message })}`);
684+
log.error(`[finishUpload][SizeDoesNotMatchError] ${JSON.stringify({ uuid, expectedSize: data_size, contactId: contact.id, message: error.message, isMultipartUpload })}`);
685+
return;
684686
}
685687

686688
if (error instanceof UploadNotFoundInStorageError) {
687-
console.error(`[finishUpload][UploadNotFoundInStorageError] Error getting bucket meta ${JSON.stringify({ uuid, contactId: contact.id, error: error.message, stack: error.stack })}`);
689+
log.error(`[finishUpload][UploadNotFoundInStorageError] ${JSON.stringify({ uuid, contactId: contact.id, error: error.message, stack: error.stack, isMultipartUpload, size: data_size })}`);
690+
return;
688691
}
689692

690-
console.error(`[finishUpload][unexpectedError] Error getting bucket meta ${JSON.stringify({ uuid, contactId: contact.id, error: error.message, stack: error.stack })}`);
693+
log.error(`[finishUpload][unexpectedError] Error getting bucket meta ${JSON.stringify({ uuid, contactId: contact.id, error: error.message, stack: error.stack })}`);
691694
});
692695
contactsThatStoreTheShard.push(contact);
693696
}
694697
}
695698

696699
if (contactsThatStoreTheShard.length === 0) {
697-
console.log('createShardAndMirrors | object %s NOT FOUND in contacts', uuid, contracts.map(c => c.nodeID));
700+
log.error('createShardAndMirrors | object %s NOT FOUND in contacts', uuid, contracts.map(c => c.nodeID));
698701
throw new Error('Shard not uploaded');
699702
}
700703

lib/engine.js

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const MongoDBStorageAdapter = require('storj-mongodb-adapter');
1919
const NetworkMessageQueue = require('./server/queues/networkQueue');
2020
const { bindNewRoutes } = require('./server/http');
2121
const { json } = require('express');
22+
const { store: requestContextStore } = require('./requestContext');
23+
const { requestLogger, unexpectedErrorLogger } = require('./server/middleware/requestLogger');
2224
const { Notifications } = require('./server/notifications');
2325
const bullQueueModule = require('./core/queue/bullQueue');
2426

@@ -224,49 +226,34 @@ Engine.prototype._configureApp = function () {
224226

225227
function bindRoute(route) {
226228
let verb = route.shift().toLowerCase();
227-
app[verb].apply(app, route);
229+
const path = route.shift();
230+
const handler = route.pop();
231+
232+
// Re-establish AsyncLocalStorage context for the
233+
// final handler. Auth middleware (storj-service-middleware)
234+
// calls next() from a MongoDB callback, which breaks
235+
// the async context chain.
236+
const wrappedHandler = (req, res, next) => {
237+
const ctx = res.locals.requestContext;
238+
if (ctx) {
239+
requestContextStore.run(ctx, () => handler(req, res, next));
240+
} else {
241+
handler(req, res, next);
242+
}
243+
};
244+
app[verb].apply(app, [path, ...route, wrappedHandler]);
228245
}
229246

230247
self._keepPendingResponsesClean();
231248

232-
const getDurationInMilliseconds = (start) => {
233-
const NS_PER_SEC = 1e9;
234-
const NS_TO_MS = 1e6;
235-
const diff = process.hrtime(start);
236-
237-
return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS;
238-
};
239-
240-
app.use((req, res, next) => {
241-
log.info(`${req.method} ${req.originalUrl} [STARTED]`);
242-
const start = process.hrtime();
243-
244-
res.on('finish', () => {
245-
const durationInMilliseconds = getDurationInMilliseconds(start);
246-
log.info(
247-
`${req.method} ${
248-
req.originalUrl
249-
} [FINISHED] ${durationInMilliseconds.toLocaleString()} ms`
250-
);
251-
});
252-
253-
res.on('close', () => {
254-
const durationInMilliseconds = getDurationInMilliseconds(start);
255-
log.info(
256-
`${req.method} ${
257-
req.originalUrl
258-
} [CLOSED] ${durationInMilliseconds.toLocaleString()} ms`
259-
);
260-
});
261-
262-
next();
263-
});
249+
app.use(requestLogger);
264250
app.use(middleware.querystring);
265251
app.use(this._trackResponseStatus.bind(this));
266252
app.use(crossorigin(corsOptions));
267253
app.use(helmet());
268254
app.get('/', this._handleRootGET.bind(this));
269255
routers.forEach(bindRoute);
256+
app.use(unexpectedErrorLogger);
270257
app.use(middleware.errorhandler({ logger: log }));
271258

272259
app.use(json());

lib/logger.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
const config = require('./config')(process.env.NODE_ENV);
88
const CONSTANTS = require('./constants');
9+
const { getContext } = require('./requestContext');
910

1011
const Winston = require('winston');
1112

@@ -48,12 +49,18 @@ module.exports = (() => {
4849
Winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
4950
Winston.format.splat(),
5051
Winston.format.printf(info => {
51-
return `${info.timestamp} ${info.level}: ${info.message}`;
52+
const { requestId, clientId } = getContext();
53+
54+
return `${info.timestamp} ${info.level} [${requestId || '-'}] [${clientId || '-'}]: ${info.message}`;
5255
})
5356
) :
5457
Winston.format.combine(
5558
Winston.format.splat(),
56-
Winston.format.printf(info => info.message)
59+
Winston.format.printf(info => {
60+
const { requestId, clientId } = getContext();
61+
62+
return `[${requestId || '-'}] [${clientId || '-'}] ${info.message}`;
63+
})
5764
);
5865

5966
return Winston.createLogger(

lib/requestContext.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { AsyncLocalStorage } from 'async_hooks';
2+
3+
interface RequestContext {
4+
requestId: string;
5+
clientId: string | undefined;
6+
}
7+
8+
const store = new AsyncLocalStorage<RequestContext>();
9+
10+
function getContext(): Partial<RequestContext> {
11+
return store.getStore() ?? {};
12+
}
13+
14+
export { store, getContext };
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Request, Response, NextFunction } from 'express';
2+
import { v4 as uuidv4 } from 'uuid';
3+
import { store as requestContextStore } from '../../requestContext';
4+
import log from '../../logger';
5+
6+
const getDurationInMilliseconds = (start: [number, number]): number => {
7+
const NS_PER_SEC = 1e9;
8+
const NS_TO_MS = 1e6;
9+
const diff = process.hrtime(start);
10+
11+
return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS;
12+
};
13+
14+
export function requestLogger(req: Request, res: Response, next: NextFunction): void {
15+
const ctx = {
16+
requestId: uuidv4(),
17+
clientId: req.headers['internxt-client'] as string | undefined,
18+
};
19+
20+
requestContextStore.run(ctx, () => {
21+
res.locals.requestContext = ctx;
22+
res.setHeader('X-Request-Id', ctx.requestId);
23+
log.info(`${req.method} ${req.originalUrl} [STARTED]`);
24+
const start = process.hrtime();
25+
26+
res.on('finish', () => {
27+
requestContextStore.run(ctx, () => {
28+
const durationInMilliseconds = getDurationInMilliseconds(start);
29+
log.info(`${req.method} ${req.originalUrl} [FINISHED] ${res.statusCode} ${durationInMilliseconds.toLocaleString()} ms`);
30+
});
31+
});
32+
33+
res.on('close', () => {
34+
requestContextStore.run(ctx, () => {
35+
const durationInMilliseconds = getDurationInMilliseconds(start);
36+
log.info(`${req.method} ${req.originalUrl} [CLOSED] ${res.statusCode} ${durationInMilliseconds.toLocaleString()} ms`);
37+
});
38+
});
39+
40+
next();
41+
});
42+
}
43+
44+
export function unexpectedErrorLogger(err: Error, req: Request, res: Response, next: NextFunction): void {
45+
log.error(`${req.method} ${req.originalUrl} [UNEXPECTED_ERROR] ${err.message}`, err.stack);
46+
next(err);
47+
}

0 commit comments

Comments
 (0)