Logs de la api como middleware
This commit is contained in:
14
deployment/database/migrations/1.1.0_LogOperations.sql
Normal file
14
deployment/database/migrations/1.1.0_LogOperations.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Loguear las operaciones de entrada-salida de la api para trazabilidad
|
||||||
|
* de errores cliente-servidor
|
||||||
|
*/
|
||||||
|
|
||||||
|
CREATE TABLE log_operations (
|
||||||
|
id BIGINT GENERATED ALWAYS AS IDENTITY,
|
||||||
|
date TIMESTAMPTZ default now(),
|
||||||
|
origin TEXT NOT NULL,
|
||||||
|
endpoint TEXT,
|
||||||
|
data JSONB
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
49
src/infrastructure/Log.repository.test.ts
Normal file
49
src/infrastructure/Log.repository.test.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { httpclient } from "config/httpclient.config";
|
||||||
|
import { pgClient } from "config/pgclient.config";
|
||||||
|
import type { ServerContext } from "domain/ServerContext";
|
||||||
|
import { after, before, describe, test } from "node:test";
|
||||||
|
import { LogRepository, type LogCreateDTO } from "./Log.repository";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("LogRepository Integration Tests", async () => {
|
||||||
|
const startDate = new Date();
|
||||||
|
|
||||||
|
const serverContext: ServerContext = {
|
||||||
|
PostgresClient: pgClient,
|
||||||
|
HttpClient: httpclient
|
||||||
|
};
|
||||||
|
|
||||||
|
const repo = new LogRepository(serverContext);
|
||||||
|
|
||||||
|
// Clean up before and after tests to ensure isolation
|
||||||
|
const cleanup = async () => {
|
||||||
|
await pgClient.query("DELETE FROM log_operations WHERE date >= $1 AND date <= $2",
|
||||||
|
[startDate.toISOString(), new Date().toISOString()]);
|
||||||
|
};
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Log must log", async () => {
|
||||||
|
const newLog: LogCreateDTO = {
|
||||||
|
data: {
|
||||||
|
card_id: "019cdd39-fc08-7417-b16d-a78794a24c01",
|
||||||
|
override: false
|
||||||
|
},
|
||||||
|
endpoint: "/nfc/generate",
|
||||||
|
origin: "serverSide"
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await repo.createLog(newLog)
|
||||||
|
|
||||||
|
assert.ok(res)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
95
src/infrastructure/Log.repository.ts
Normal file
95
src/infrastructure/Log.repository.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { tryCatch, type Result } from "domain/Result";
|
||||||
|
import type { ServerContext } from "domain/ServerContext";
|
||||||
|
|
||||||
|
export type Log = {
|
||||||
|
id: string,
|
||||||
|
date: Date,
|
||||||
|
origin?: string,
|
||||||
|
endpoint: string,
|
||||||
|
data: Record<string, any>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogCreateDTO = Pick<Log, "origin" | "endpoint" | "data">
|
||||||
|
|
||||||
|
export class LogRepository {
|
||||||
|
private ctx: ServerContext;
|
||||||
|
constructor(serverContext: ServerContext) {
|
||||||
|
this.ctx = serverContext
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readBetween(start: Date, end: Date): Promise<Result<string, Log[]>> {
|
||||||
|
const query = `
|
||||||
|
SELECT id, date, origin, endpint, data
|
||||||
|
FROM log_operations
|
||||||
|
WHERE date >= $1 AND date <= $2
|
||||||
|
ORDER BY date DESC
|
||||||
|
`;
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
start.toISOString(), // ISOString asegura compatibilidad con TIMESTAMPTZ
|
||||||
|
end.toISOString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
const prom = this.ctx.PostgresClient.query<Log>(query, values)
|
||||||
|
const res = await tryCatch(prom)
|
||||||
|
|
||||||
|
if (res.error != undefined) {
|
||||||
|
return {
|
||||||
|
error: res.error.msg.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: res.data.rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createLog(newLog: LogCreateDTO): Promise<Result<string, Log>> {
|
||||||
|
const conn = await this.ctx.PostgresClient.connect()
|
||||||
|
await conn.query("BEGIN")
|
||||||
|
|
||||||
|
const { origin, endpoint, data } = newLog
|
||||||
|
|
||||||
|
const query = `
|
||||||
|
INSERT INTO log_operations (
|
||||||
|
origin,
|
||||||
|
endpoint,
|
||||||
|
data
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
) RETURNING
|
||||||
|
id, date, origin, endpoint, data
|
||||||
|
`
|
||||||
|
const values = [origin, endpoint, JSON.stringify(data)]
|
||||||
|
const promiseRes = conn.query<Log>(query, values)
|
||||||
|
const res = await tryCatch(promiseRes)
|
||||||
|
|
||||||
|
if (res.error != undefined) {
|
||||||
|
console.error("[x] Error logueando", res.error.msg)
|
||||||
|
await conn.query("ROLLBACK")
|
||||||
|
conn.release();
|
||||||
|
return {
|
||||||
|
error: String(res.error.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdLogs = res.data.rows
|
||||||
|
|
||||||
|
if (createdLogs.length == 0 || createdLogs[0] == undefined) {
|
||||||
|
await conn.query("ROLLBACK")
|
||||||
|
conn.release();
|
||||||
|
return {
|
||||||
|
error: "Error creating the Log, insert response was empty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.query("COMMIT")
|
||||||
|
conn.release()
|
||||||
|
return {
|
||||||
|
data: createdLogs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,6 @@ await describe("NfcRepository Integration Tests", async () => {
|
|||||||
assert.fail(`findActivationCodes failed: ${result.error}`);
|
assert.fail(`findActivationCodes failed: ${result.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
assert.ok(result.data, "Data should be returned");
|
assert.ok(result.data, "Data should be returned");
|
||||||
assert.strictEqual(result.data.length, 0);
|
assert.strictEqual(result.data.length, 0);
|
||||||
});
|
});
|
||||||
@@ -114,7 +113,6 @@ await describe("NfcRepository Integration Tests", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.ok(result.data, "Data should be returned");
|
assert.ok(result.data, "Data should be returned");
|
||||||
|
|
||||||
assert.ok(Array.isArray(result.data));
|
assert.ok(Array.isArray(result.data));
|
||||||
|
|
||||||
const first = result.data[0];
|
const first = result.data[0];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { ActivationCodeDTO } from "domain/NfcDTOs.js";
|
import type { ActivationCodeDTO } from "domain/NfcDTOs.js";
|
||||||
import type { Result } from "domain/Result.js";
|
import type { Result } from "domain/Result.js";
|
||||||
import type { ServerContext } from "domain/ServerContext.js";
|
import type { ServerContext } from "domain/ServerContext.js";
|
||||||
import { constrainedMemory } from "node:process";
|
|
||||||
|
|
||||||
// TODO: Pasar a Result<E,T>
|
// TODO: Pasar a Result<E,T>
|
||||||
|
|
||||||
|
|||||||
68
src/main.ts
68
src/main.ts
@@ -11,27 +11,13 @@ import { pgClient } from 'config/pgclient.config.js';
|
|||||||
import { NfcController } from 'aplication/Nfc.controller.js';
|
import { NfcController } from 'aplication/Nfc.controller.js';
|
||||||
import { NfcUsecases } from 'aplication/Nfc.usecases.js';
|
import { NfcUsecases } from 'aplication/Nfc.usecases.js';
|
||||||
import { NfcRepository } from 'infrastructure/Nfc.repository.js';
|
import { NfcRepository } from 'infrastructure/Nfc.repository.js';
|
||||||
|
import { LogRepository, type LogCreateDTO } from 'infrastructure/Log.repository.js';
|
||||||
|
|
||||||
const PORT = env.PORT
|
const PORT = env.PORT
|
||||||
const HOSTNAME = env.HOST
|
const HOSTNAME = env.HOST
|
||||||
|
|
||||||
assert(HOSTNAME != undefined)
|
assert(HOSTNAME != undefined)
|
||||||
|
|
||||||
// test
|
|
||||||
const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const method = req.method;
|
|
||||||
const url = req.url;
|
|
||||||
const body = req.body;
|
|
||||||
|
|
||||||
// 'origin' comes from the request headers
|
|
||||||
const origin = req.get('origin') || 'No Origin (likely direct request/Server-side)';
|
|
||||||
|
|
||||||
console.log(`[${timestamp}] ${method} ${url} - Origin: ${origin} - Body: ${JSON.stringify(body)}`);
|
|
||||||
|
|
||||||
// Crucial: Call next() so the request doesn't hang!
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Instancias de las dependencias
|
// Instancias de las dependencias
|
||||||
|
|
||||||
@@ -41,6 +27,7 @@ const serverContext: ServerContext = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nfcRepository = new NfcRepository(serverContext)
|
const nfcRepository = new NfcRepository(serverContext)
|
||||||
|
const logRepository = new LogRepository(serverContext)
|
||||||
|
|
||||||
const nfcUsecases = new NfcUsecases({
|
const nfcUsecases = new NfcUsecases({
|
||||||
serverContext,
|
serverContext,
|
||||||
@@ -52,6 +39,56 @@ const ncfControllers = new NfcController({
|
|||||||
nfcUsecases
|
nfcUsecases
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// middleware
|
||||||
|
|
||||||
|
const requestLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const method = req.method;
|
||||||
|
const url = req.url;
|
||||||
|
const body = req.body;
|
||||||
|
|
||||||
|
// 'origin' comes from the request headers
|
||||||
|
const origin = req.get('origin') || 'No Origin (likely direct request/Server-side)';
|
||||||
|
const log: LogCreateDTO = {
|
||||||
|
data: req.body,
|
||||||
|
endpoint: method + " " + req.url,
|
||||||
|
origin: origin
|
||||||
|
}
|
||||||
|
logRepository.createLog(log).then().catch(e => console.error("Error logueando ", log, e))
|
||||||
|
console.log(`[${timestamp}] ${method} ${url} - Origin: ${origin} - Body: ${JSON.stringify(body)}`);
|
||||||
|
|
||||||
|
// Crucial: Call next() so the request doesn't hang!
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseLogger = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const start = Date.now();
|
||||||
|
const oldSend = res.send;
|
||||||
|
|
||||||
|
|
||||||
|
res.send = function (data: any): Response {
|
||||||
|
// @ts-ignore: Si, es poco ortodoxo
|
||||||
|
res.sendBody = data; // Store the result data
|
||||||
|
return oldSend.apply(res, [data]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listen for the 'finish' event to log after the response is sent
|
||||||
|
res.on('finish', () => {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
const log: LogCreateDTO = {
|
||||||
|
// @ts-ignore: Por lo de antes
|
||||||
|
data: res.sendBody,
|
||||||
|
endpoint: req.method + " " + req.originalUrl,
|
||||||
|
origin: "SERVER"
|
||||||
|
}
|
||||||
|
logRepository.createLog(log)
|
||||||
|
console.log(`[${timestamp} ${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`);
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
// Rutas
|
// Rutas
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
@@ -76,6 +113,7 @@ app.use(cors({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
app.use(requestLogger)
|
app.use(requestLogger)
|
||||||
|
app.use(responseLogger)
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/', router);
|
app.use('/', router);
|
||||||
|
|||||||
Reference in New Issue
Block a user