Skip to content

Commit 335241b

Browse files
authored
Merge pull request #715 from SatyamPandey-07/feature/logging-telemetry-713
Fix #713: Implement Centralized Structured Logging & Telemetry Pipeline
2 parents 7a47491 + b15dcf9 commit 335241b

11 files changed

Lines changed: 403 additions & 1 deletion

File tree

LOGGING_ARCHITECTURE.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Centralized Structured Logging & Telemetry Pipeline
2+
3+
## 🚀 Overview
4+
Issue #713 overhauls the application's logging infrastructure, moving from primitive `console.log` statements to a professional **Structured JSON Logging System**. This allows for high-resolution request tracing, automated monitoring, and easier debugging across distributed components.
5+
6+
## 🏗️ Technical Architecture
7+
8+
### 1. Structured Logger (`utils/structuredLogger.js`)
9+
The core engine that produces JSON-formatted logs.
10+
- **AsyncLocalStorage**: Uses Node.js `AsyncLocalStorage` to automatically propagate Trace IDs (Correlation IDs) across asynchronous operations without manual prop-drilling.
11+
- **Log Levels**: Supports `DEBUG`, `INFO`, `WARN`, `ERROR`, and `CRITICAL`.
12+
- **Metadata Support**: Every log can include a dynamic metadata object for rich context.
13+
14+
### 2. Request Correlation (`middleware/requestCorrelation.js`)
15+
- Generates a `traceId` for every incoming HTTP request.
16+
- Injects this ID into the logger's storage context.
17+
- Returns the ID as a response header (`x-trace-id`) for better client-side troubleshooting.
18+
19+
### 3. HTTP Traffic Interceptor (`middleware/httpLogger.js`)
20+
- Records every request and response.
21+
- Calculates and logs the **Response Latency** in milliseconds.
22+
- Automatically adjusts log levels based on HTTP status codes (4xx = Warn, 5xx = Error).
23+
24+
### 4. Telemetry Sink (`utils/telemetryExporter.js`)
25+
- Implements a buffered export pattern.
26+
- Batches logs to reduce I/O overhead.
27+
- Simulates integration with professional observability platforms (Datadog/NewRelic).
28+
29+
## 📂 File Structure
30+
- `utils/structuredLogger.js`: The JSON logger engine.
31+
- `middleware/requestCorrelation.js`: Context provider.
32+
- `middleware/httpLogger.js`: Network telemetry.
33+
- `utils/telemetryExporter.js`: Centralized export.
34+
- `routes/telemetry.js`: Visibility API.
35+
- `jobs/logRotator.js`: Maintenance & disk cleanup.
36+
37+
## 📊 Sample Log Entry
38+
```json
39+
{
40+
"timestamp": "2026-02-19T13:14:02.123Z",
41+
"level": "INFO",
42+
"message": "Completed POST /api/expenses [201]",
43+
"traceId": "a1b2c3d4-e5f6-7890",
44+
"userId": "648f12a3b4c5",
45+
"type": "http_response",
46+
"durationMs": 45,
47+
"environment": "production"
48+
}
49+
```
50+
51+
## ✅ Implementation Checklist
52+
- [x] JSON-structured logger with `AsyncLocalStorage`.
53+
- [x] Request Trace ID propagation middleware.
54+
- [x] Automatic HTTP latency logging.
55+
- [x] Batch log exporter with buffering logic.
56+
- [x] Log rotation and archival background job.
57+
- [x] Refactored existing services to use structured logs.

jobs/logRotator.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const cron = require('node-cron');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const logger = require('../utils/structuredLogger');
5+
6+
/**
7+
* Log Rotator Job
8+
* Issue #713: Prevents log files from consuming infinite disk space.
9+
*/
10+
class LogRotator {
11+
constructor() {
12+
this.logDir = path.join(process.cwd(), 'logs');
13+
}
14+
15+
start() {
16+
console.log('[LogRotator] Initializing log rotation worker...');
17+
18+
// Run every night at midnight
19+
cron.schedule('0 0 * * *', () => {
20+
this.rotate();
21+
});
22+
}
23+
24+
rotate() {
25+
logger.info('Starting nightly log rotation sequence...');
26+
27+
try {
28+
const files = fs.readdirSync(this.logDir);
29+
const timestamp = new Date().toISOString().replace(/:/g, '-');
30+
31+
for (const file of files) {
32+
if (file.endsWith('.log') && !file.includes('_archive_')) {
33+
const oldPath = path.join(this.logDir, file);
34+
const newPath = path.join(this.logDir, `${file.replace('.log', '')}_archive_${timestamp}.log`);
35+
36+
// In a real system, we might GZIP this, or move it to S3
37+
fs.renameSync(oldPath, newPath);
38+
}
39+
}
40+
41+
// Also clean up archives older than 7 days
42+
this.cleanupArchives();
43+
44+
logger.info('Log rotation completed successfully.');
45+
} catch (err) {
46+
logger.error('Log rotation failed', { error: err.message });
47+
}
48+
}
49+
50+
cleanupArchives() {
51+
const files = fs.readdirSync(this.logDir);
52+
const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
53+
54+
for (const file of files) {
55+
if (file.includes('_archive_')) {
56+
const filePath = path.join(this.logDir, file);
57+
const stats = fs.statSync(filePath);
58+
59+
if (stats.mtimeMs < sevenDaysAgo) {
60+
fs.unlinkSync(filePath);
61+
}
62+
}
63+
}
64+
}
65+
}
66+
67+
module.exports = new LogRotator();

logs/info.log

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"timestamp":"2026-02-19T13:19:09.339Z","level":"INFO","message":"System startup","traceId":"system","userId":"anonymous","component":"test-suite","environment":"development","version":"1.0.0"}
2+
{"timestamp":"2026-02-19T13:19:09.342Z","level":"INFO","message":"User action performed","traceId":"test-trace-123","userId":"user-789","action":"click_login","environment":"development","version":"1.0.0"}

logs/warn.log

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"timestamp":"2026-02-19T13:19:09.454Z","level":"WARN","message":"Asynchronous warning","traceId":"test-trace-123","userId":"user-789","latency":500,"environment":"development","version":"1.0.0"}

middleware/httpLogger.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const logger = require('../utils/structuredLogger');
2+
3+
/**
4+
* HTTP Request/Response Logger
5+
* Issue #713: Standardizes logging for all network traffic.
6+
*/
7+
const httpLogger = (req, res, next) => {
8+
const startTime = Date.now();
9+
10+
// Log the incoming request
11+
logger.info(`Incoming ${req.method} ${req.originalUrl}`, {
12+
type: 'http_request',
13+
userAgent: req.get('user-agent'),
14+
referrer: req.get('referrer'),
15+
ip: req.ip
16+
});
17+
18+
// Hook into the finish event to log the response
19+
res.on('finish', () => {
20+
const duration = Date.now() - startTime;
21+
const level = res.statusCode >= 500 ? 'error' : (res.statusCode >= 400 ? 'warn' : 'info');
22+
23+
logger[level](`Completed ${req.method} ${req.originalUrl} [${res.statusCode}]`, {
24+
type: 'http_response',
25+
statusCode: res.statusCode,
26+
durationMs: duration,
27+
contentLength: res.get('content-length')
28+
});
29+
});
30+
31+
next();
32+
};
33+
34+
module.exports = httpLogger;

middleware/requestCorrelation.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { v4: uuidv4 } = require('uuid');
2+
const logger = require('../utils/structuredLogger');
3+
const crypto = require('crypto');
4+
5+
/**
6+
* Request Correlation Middleware
7+
* Issue #713: Ensures every request has a unique Trace ID for log aggregation.
8+
*/
9+
const requestCorrelation = (req, res, next) => {
10+
// Generate or propagate Trace ID
11+
const traceId = req.header('x-trace-id') || crypto.randomUUID();
12+
13+
// Set response header for client-side tracking
14+
res.setHeader('x-trace-id', traceId);
15+
16+
const context = {
17+
traceId,
18+
startTime: Date.now(),
19+
ip: req.ip,
20+
method: req.method,
21+
url: req.originalUrl,
22+
userId: req.user ? req.user._id : null
23+
};
24+
25+
// Run the rest of the request within the async context
26+
logger.getStorage().run(context, () => {
27+
next();
28+
});
29+
};
30+
31+
module.exports = requestCorrelation;

routes/telemetry.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const express = require('express');
2+
const router = express.Router();
3+
const fs = require('fs');
4+
const path = require('path');
5+
const auth = require('../middleware/auth');
6+
const logger = require('../utils/structuredLogger');
7+
8+
/**
9+
* Telemetry API
10+
* Issue #713: Provides visibility into the centralized logging pipeline.
11+
*/
12+
13+
router.get('/stats', auth, async (req, res) => {
14+
try {
15+
const logDir = path.join(process.cwd(), 'logs');
16+
const files = fs.readdirSync(logDir);
17+
18+
const stats = files.map(file => {
19+
const filePath = path.join(logDir, file);
20+
const size = fs.statSync(filePath).size;
21+
return {
22+
level: file.split('.')[0],
23+
sizeBytes: size,
24+
lastUpdated: fs.statSync(filePath).mtime
25+
};
26+
});
27+
28+
res.json({
29+
success: true,
30+
data: {
31+
activeLogs: stats,
32+
loggerConfig: {
33+
minLevel: process.env.LOG_LEVEL || 'INFO',
34+
environment: process.env.NODE_ENV || 'development'
35+
}
36+
}
37+
});
38+
} catch (err) {
39+
logger.error('Failed to retrieve telemetry stats', { error: err.message });
40+
res.status(500).json({ success: false, error: 'Failed to access log subsystem' });
41+
}
42+
});
43+
44+
router.get('/tail/:level', auth, async (req, res) => {
45+
const { level } = req.params;
46+
const logFile = path.join(process.cwd(), 'logs', `${level.toLowerCase()}.log`);
47+
48+
if (!fs.existsSync(logFile)) {
49+
return res.status(404).json({ success: false, error: 'Log level file not found' });
50+
}
51+
52+
try {
53+
// Simple tail: last 50 lines
54+
const content = fs.readFileSync(logFile, 'utf8');
55+
const lines = content.trim().split('\n').slice(-50);
56+
const logs = lines.map(line => JSON.parse(line));
57+
58+
res.json({ success: true, data: logs });
59+
} catch (err) {
60+
res.status(500).json({ success: false, error: 'Failed to read log file' });
61+
}
62+
});
63+
64+
module.exports = router;

server.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ require('dotenv').config();
88
const app = express();
99
const server = http.createServer(app);
1010

11+
// Initialize logging context at the very start
12+
app.use(require('./middleware/requestCorrelation'));
13+
app.use(require('./middleware/httpLogger'));
14+
15+
1116
/* ================================
1217
SECURITY
1318
================================ */
@@ -72,6 +77,8 @@ async function connectDatabase() {
7277
require('./jobs/forecastRetrainer').start();
7378
require('./jobs/taxonomyAuditor').start();
7479
require('./jobs/conflictCleaner').start();
80+
require('./jobs/logRotator').start();
81+
7582

7683

7784
console.log('✓ Cron jobs initialized');
@@ -99,6 +106,10 @@ app.use('/api/analytics', require('./routes/analytics'));
99106
app.use('/api/export', require('./routes/export'));
100107
app.use('/api/forecasting', require('./routes/forecasting'));
101108
app.use('/api/governance', require('./routes/governance'));
109+
app.use('/api/taxonomy', require('./routes/taxonomy'));
110+
app.use('/api/sync', require('./routes/syncManager'));
111+
app.use('/api/telemetry', require('./routes/telemetry'));
112+
102113

103114

104115

services/validationEngine.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const remediationRules = require('../utils/remediationRules');
22
const ValidationLog = require('../models/ValidationLog');
33
const crypto = require('crypto');
4+
const logger = require('../utils/structuredLogger');
5+
46

57

68
/**
@@ -80,7 +82,7 @@ class ValidationEngine {
8082
};
8183

8284
} catch (error) {
83-
console.error('[ValidationEngine] Pipeline failure:', error);
85+
logger.error('Validation pipeline failure', { error: error.message, stack: error.stack });
8486
throw error;
8587
}
8688
}

utils/structuredLogger.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const { AsyncLocalStorage } = require('async_hooks');
4+
5+
/**
6+
* Global store for request-scoped context (Correlation IDs)
7+
*/
8+
const storage = new AsyncLocalStorage();
9+
10+
/**
11+
* Structured Logger Engine
12+
* Issue #713: Implements high-performance JSON-based structured logging.
13+
*/
14+
class StructuredLogger {
15+
constructor() {
16+
this.logDir = path.join(process.cwd(), 'logs');
17+
if (!fs.existsSync(this.logDir)) {
18+
fs.mkdirSync(this.logDir);
19+
}
20+
21+
this.levels = {
22+
DEBUG: 0,
23+
INFO: 1,
24+
WARN: 2,
25+
ERROR: 3,
26+
CRITICAL: 4
27+
};
28+
29+
this.minLevel = process.env.LOG_LEVEL || 'INFO';
30+
}
31+
32+
/**
33+
* Internal method to write the log entry
34+
*/
35+
_write(level, message, metadata = {}) {
36+
if (this.levels[level] < this.levels[this.minLevel]) return;
37+
38+
const context = storage.getStore() || {};
39+
const logEntry = {
40+
timestamp: new Date().toISOString(),
41+
level,
42+
message,
43+
traceId: context.traceId || 'system',
44+
userId: context.userId || 'anonymous',
45+
...metadata,
46+
environment: process.env.NODE_ENV || 'development',
47+
version: process.env.APP_VERSION || '1.0.0'
48+
};
49+
50+
const logString = JSON.stringify(logEntry);
51+
52+
// Output to console for cloud log collectors (like Vercel/AWS)
53+
if (level === 'ERROR' || level === 'CRITICAL') {
54+
console.error(logString);
55+
} else {
56+
console.log(logString);
57+
}
58+
59+
// Also write to local file for development audit trails
60+
const logFile = path.join(this.logDir, `${level.toLowerCase()}.log`);
61+
fs.appendFile(logFile, logString + '\n', (err) => {
62+
if (err) console.error('Failed to write to log file:', err);
63+
});
64+
}
65+
66+
info(msg, meta) { this._write('INFO', msg, meta); }
67+
debug(msg, meta) { this._write('DEBUG', msg, meta); }
68+
warn(msg, meta) { this._write('WARN', msg, meta); }
69+
error(msg, meta) { this._write('ERROR', msg, meta); }
70+
critical(msg, meta) { this._write('CRITICAL', msg, meta); }
71+
72+
/**
73+
* Provide access to the storage for middleware integration
74+
*/
75+
getStorage() {
76+
return storage;
77+
}
78+
}
79+
80+
module.exports = new StructuredLogger();

0 commit comments

Comments
 (0)