-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(metrics): add metrics logging and configuration support (#1559)
* feat(metrics): add metrics logging and configuration support * refactor folder structure * implement accesslogs service methods * minor fixes * metrics service init part * remove metrics modules and send logs to the graylog * renamings * remove unused package * update example config * add comments for conditional module loading and env variable reuse * improved error message for when no/wrong format json file provided
- Loading branch information
1 parent
ca918de
commit d61c826
Showing
7 changed files
with
160 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
{ | ||
"include": [ | ||
{ | ||
"path": "*", | ||
"method": 0, | ||
"version": "3" | ||
}, | ||
{ | ||
"path": "datasets/fullquery", | ||
"method": 0, | ||
"version": "3" | ||
}, | ||
{ | ||
"path": "datasets/:id", | ||
"method": 0, | ||
"version": "3" | ||
} | ||
], | ||
"exclude": [ | ||
{ | ||
"path": "datasets/fullfacet", | ||
"method": 0, | ||
"version": "3" | ||
}, | ||
{ | ||
"path": "datasets/metadataKeys", | ||
"method": 0, | ||
"version": "3" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Logger, MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; | ||
import { ConfigModule, ConfigService } from "@nestjs/config"; | ||
import { AccessTrackingMiddleware } from "./middlewares/accessTracking.middleware"; | ||
import { JwtModule } from "@nestjs/jwt"; | ||
|
||
@Module({ | ||
imports: [ConfigModule, JwtModule], | ||
exports: [], | ||
}) | ||
export class MetricsModule implements NestModule { | ||
constructor(private readonly configService: ConfigService) {} | ||
|
||
configure(consumer: MiddlewareConsumer) { | ||
const { include = [], exclude = [] } = | ||
this.configService.get("metrics.config") || {}; | ||
if (!include.length && !exclude.length) { | ||
Logger.error( | ||
'Metrics middleware requires at least one "include" or "exclude" path in the metricsConfig.json file.', | ||
"MetricsModule", | ||
); | ||
return; | ||
} | ||
|
||
try { | ||
consumer | ||
.apply(AccessTrackingMiddleware) | ||
.exclude(...exclude) | ||
.forRoutes(...include); | ||
Logger.log("Start collecting metrics", "MetricsModule"); | ||
} catch (error) { | ||
Logger.error("Error configuring metrics middleware", error); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { Injectable, Logger, NestMiddleware } from "@nestjs/common"; | ||
import { Request, Response, NextFunction } from "express"; | ||
import { JwtService } from "@nestjs/jwt"; | ||
import { parse } from "url"; | ||
|
||
@Injectable() | ||
export class AccessTrackingMiddleware implements NestMiddleware { | ||
private requestCache = new Map<string, number>(); // Cache to store recent requests | ||
private logIntervalDuration = 1000; // Log every 1 second to prevent spam | ||
private cacheResetInterval = 10 * 60 * 1000; // Clear cache every 10 minutes to prevent memory leak | ||
|
||
constructor(private readonly jwtService: JwtService) { | ||
this.startCacheResetInterval(); | ||
} | ||
use(req: Request, res: Response, next: NextFunction) { | ||
const { query, pathname } = parse(req.originalUrl, true); | ||
|
||
const userAgent = req.headers["user-agent"]; | ||
// TODO: Better to use a library for this? | ||
const isBot = userAgent ? /bot|crawl|spider|slurp/i.test(userAgent) : false; | ||
|
||
if (!pathname || isBot) return; | ||
|
||
const startTime = Date.now(); | ||
const authHeader = req.headers.authorization; | ||
const originIp = req.socket.remoteAddress; | ||
const userId = this.parseToken(authHeader); | ||
|
||
const cacheKeyIdentifier = `${userId}-${originIp}-${pathname}`; | ||
|
||
res.on("finish", () => { | ||
const statusCode = res.statusCode; | ||
if (statusCode === 304) return; | ||
|
||
const responseTime = Date.now() - startTime; | ||
|
||
const lastHitTime = this.requestCache.get(cacheKeyIdentifier); | ||
|
||
// Log only if the request was not recently logged | ||
if (!lastHitTime || Date.now() - lastHitTime > this.logIntervalDuration) { | ||
Logger.log("SciCatAccessLogs", { | ||
userId, | ||
originIp, | ||
endpoint: pathname, | ||
query: query, | ||
statusCode, | ||
responseTime, | ||
}); | ||
|
||
this.requestCache.set(cacheKeyIdentifier, Date.now()); | ||
} | ||
}); | ||
|
||
next(); | ||
} | ||
|
||
private parseToken(authHeader?: string) { | ||
if (!authHeader) return "anonymous"; | ||
const token = authHeader.split(" ")[1]; | ||
if (!token) return "anonymous"; | ||
|
||
try { | ||
const { id } = this.jwtService.decode(token); | ||
return id; | ||
} catch (error) { | ||
Logger.error("Error parsing token-> AccessTrackingMiddleware", error); | ||
return null; | ||
} | ||
} | ||
|
||
private startCacheResetInterval() { | ||
setInterval(() => { | ||
this.requestCache.clear(); | ||
}, this.cacheResetInterval); | ||
} | ||
} |