Skip to content

Commit

Permalink
feat: add logger service to backend
Browse files Browse the repository at this point in the history
  • Loading branch information
frederic-maury committed Dec 25, 2024
1 parent 6983c76 commit 8ed046f
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 14 deletions.
3 changes: 2 additions & 1 deletion backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

src/services/pin/pins
src/services/pin/pins
logs/
37 changes: 28 additions & 9 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"express-rate-limit": "^7.5.0",
"node-cache": "^5.1.2"
},
"devDependencies": {
Expand Down
32 changes: 32 additions & 0 deletions backend/src/controllers/logger.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Request, Response } from 'express';
import rateLimit from 'express-rate-limit';
import LoggerService from '../services/logger.service';

const DEFAULT_LOG_LIMIT_WINDOW_MS = 15;
const DEFAULT_LOG_LIMIT_MAX = 40;

export const logLimiter = rateLimit({
windowMs: (process.env.LOG_LIMIT_WINDOW_MS ? parseInt(process.env.LOG_LIMIT_WINDOW_MS) : DEFAULT_LOG_LIMIT_WINDOW_MS) * 60 * 1000,
max: process.env.LOG_LIMIT_MAX ? parseInt(process.env.LOG_LIMIT_MAX) : DEFAULT_LOG_LIMIT_MAX,
message: { message: 'Too many log requests, please try again later' }
});

export async function log(req: Request, res: Response): Promise<Response> {
try {
const { level = 'INFO', message, metadata } = req.body;

if (!message) {
return res.status(400).json({ message: 'Message is required' });
}

if (!['INFO', 'ERROR', 'WARN'].includes(level)) {
return res.status(400).json({ message: 'Invalid log level' });
}

await LoggerService.log(level, message, metadata);
return res.status(200).json({ message: 'Log entry created successfully' });
} catch (error) {
console.error('Error creating log entry:', error);
return res.status(500).json({ message: 'Error creating log entry' });
}
}
8 changes: 5 additions & 3 deletions backend/src/controllers/properties.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { NextFunction, Request, Response } from "express";
import axios from "axios";
import CacheService from "../services/cache.service";
import { NextFunction, Request, Response } from "express";

const PropertiesCacheKey = 'properties'
const PropertiesCacheKey = 'properties';

const DEFAULT_PROPERTIES_CACHE_TTL = 60;

// TTL is in seconds, 1 hour
const cachedTime = 60 * 60
const cachedTime = (process.env.PROPERTIES_CACHE_TTL ? parseInt(process.env.PROPERTIES_CACHE_TTL) * 60 : 60) * 60;

export async function get(req: Request, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>>> {
try {
Expand Down
4 changes: 3 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import express from 'express';
import dotEnv from 'dotenv';
import cors from 'cors';
import bodyParser from 'body-parser';
import compression from 'compression';
import CacheService from './services/cache.service';
import PropertiesRouter from './routes/properties.route';
import compression from 'compression';
import LoggerRouter from './routes/logger.route';
import { initPinService } from './services/pin/pin.service';

dotEnv.config();
Expand Down Expand Up @@ -43,6 +44,7 @@ app.get('/', (req, res) => {
});

app.use('/properties', PropertiesRouter);
app.use('/log', LoggerRouter);

initPinService()
.then(() => {
Expand Down
27 changes: 27 additions & 0 deletions backend/src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Request, Response, NextFunction } from 'express';
import dotenv from 'dotenv';
import crypto from 'crypto';

dotenv.config();

function hashToken(token: string): string {
return crypto.createHash('sha256').update(token).digest('hex');
}

export const authenticateToken = (req: Request, res: Response, next: NextFunction) => {
const actual_token = process.env.API_TOKEN
? hashToken(process.env.API_TOKEN)
: '92f5b24048bd100fc7cb1dc770f0d72e1ae4300eb9e13f642304ee71e8515ef4';
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

if (!token) {
return res.status(401).json({ message: 'No token provided' });
}

if (token !== actual_token) {
return res.status(403).json({ message: 'Invalid token' });
}

next();
};
10 changes: 10 additions & 0 deletions backend/src/routes/logger.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Router } from "express";
import { authenticateToken } from "../middlewares/auth.middleware";
import { log } from "../controllers/logger.controller";
import { logLimiter } from "../controllers/logger.controller";

const LoggerRouter = Router();

LoggerRouter.post('/', authenticateToken, logLimiter, log);

export default LoggerRouter;
111 changes: 111 additions & 0 deletions backend/src/services/logger.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import fs from 'fs/promises';
import path from 'path';
import { createWriteStream, existsSync, mkdirSync, WriteStream } from 'fs';

export class LoggerService {
private static instance: LoggerService;
private readonly logDir: string;
private readonly maxFileSize: number; // in bytes
private readonly maxFiles: number;
private currentLogFile: string;
private writeStream: WriteStream | null;

private constructor() {
this.logDir = path.join(__dirname, '../../logs');
this.maxFileSize = 5 * 1024 * 1024; // 5MB
this.maxFiles = 5;
this.currentLogFile = path.join(this.logDir, 'app.log');
this.writeStream = null;
this.initializeLogDirectory();
}

public static getInstance(): LoggerService {
if (!LoggerService.instance) {
LoggerService.instance = new LoggerService();
}
return LoggerService.instance;
}

private initializeLogDirectory(): void {
if (!existsSync(this.logDir)) {
mkdirSync(this.logDir, { recursive: true });
}
}

private async rotateLog(): Promise<void> {
if (this.writeStream) {
this.writeStream.end();
this.writeStream = null;
}

const files = await fs.readdir(this.logDir);
const logFiles = files
.filter(file => file.startsWith('app.log'))
.sort((a, b) => b.localeCompare(a));

// Rotate existing files
for (const file of logFiles) {
const filePath = path.join(this.logDir, file);
const stats = await fs.stat(filePath);

if (file === 'app.log' && stats.size >= this.maxFileSize) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
await fs.rename(filePath, path.join(this.logDir, `app.log.${timestamp}`));
}
}

// Remove oldest files if we exceed maxFiles
if (logFiles.length >= this.maxFiles) {
const filesToRemove = logFiles.slice(this.maxFiles - 1);
for (const file of filesToRemove) {
await fs.unlink(path.join(this.logDir, file));
}
}
}

private getWriteStream(): WriteStream {
if (!this.writeStream) {
this.writeStream = createWriteStream(this.currentLogFile, { flags: 'a' });
}
return this.writeStream;
}

public async log(level: 'INFO' | 'ERROR' | 'WARN', message: string, metadata?: any): Promise<void> {
try {
const stats = existsSync(this.currentLogFile)
? await fs.stat(this.currentLogFile)
: { size: 0 };

if (stats.size >= this.maxFileSize) {
await this.rotateLog();
}

const timestamp = new Date().toISOString();
const logEntry = {
timestamp,
level,
message,
...(metadata && { metadata }),
};

const logLine = `${JSON.stringify(logEntry)}\n`;
this.getWriteStream().write(logLine);
} catch (error) {
console.error('Error writing to log file:', error);
}
}

public async info(message: string, metadata?: any): Promise<void> {
return this.log('INFO', message, metadata);
}

public async error(message: string, metadata?: any): Promise<void> {
return this.log('ERROR', message, metadata);
}

public async warn(message: string, metadata?: any): Promise<void> {
return this.log('WARN', message, metadata);
}
}

export default LoggerService.getInstance();

0 comments on commit 8ed046f

Please sign in to comment.