Compare commits
2 Commits
main
...
WEBINT-175
| Author | SHA1 | Date | |
|---|---|---|---|
| 30f1819a4e | |||
| cbbc0f6edb |
25
packages/sim-rabbit-monitor/index.ts
Normal file
25
packages/sim-rabbit-monitor/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import express from "express"
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import { env } from "packages/sim-shared/config/env/index.js"
|
||||
|
||||
const PORT = env.API_PORT
|
||||
const HOSTNAME = "0.0.0.0"
|
||||
const app = express()
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use("/docs", express.static(path.join(process.cwd(), '../../docs')))
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
res.status(200).json({ status: "ok" })
|
||||
})
|
||||
|
||||
app.listen(PORT, HOSTNAME, () => {
|
||||
console.log("[o] Servidor iniciado en el puerto %d", PORT)
|
||||
})
|
||||
export default {}
|
||||
15
packages/sim-shared/aplication/Rabbit.usecases.ts
Normal file
15
packages/sim-shared/aplication/Rabbit.usecases.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { RabbitManagementClient } from "../infrastructure/RabbitManagementClient.js";
|
||||
import { Queue } from "../domain/Queue.js";
|
||||
|
||||
export class RabbitUseCases {
|
||||
private client: RabbitManagementClient
|
||||
|
||||
constructor(client: RabbitManagementClient) {
|
||||
this.client = client
|
||||
}
|
||||
|
||||
public async getQueuesStatus(): Promise<Queue[]> {
|
||||
const queues = await this.client.getQueues()
|
||||
return queues.sort((a, b) => b.messages - a.messages)
|
||||
}
|
||||
}
|
||||
20
packages/sim-shared/config/env/index.ts
vendored
Normal file
20
packages/sim-shared/config/env/index.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { loadEnvFile } from "node:process";
|
||||
import path from "node:path";
|
||||
|
||||
|
||||
loadEnvFile(path.join("../../.env")) // Global
|
||||
|
||||
export const env = {
|
||||
ENVIRONMENT: process.env.ENVIORMENT,
|
||||
API_PORT: parseInt(process.env.API_PORT ?? "3000"),
|
||||
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST ?? "localhost"),
|
||||
RABBITMQ_USER: String(process.env.RABBITMQ_USER ?? "test"),
|
||||
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD ?? "test"),
|
||||
RABBITMQ_EXCHANGE: String(process.env.RABBITMQ_EXCHANGE ?? "/"),
|
||||
RABBITMQ_PORT: parseInt(process.env.RABBITMQ_PORT ?? "5672"),
|
||||
RABBITMQ_MODULENAME: process.env.MODULENAME,
|
||||
RABBITMQ_TTL: process.env.RABBITMQ_TTL,
|
||||
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
|
||||
RABBITMQ_RETRY_INTERVAL: process.env.RABBITMQ_INTERVAL,
|
||||
RABBITMQ_VHOST: String(process.env.RABBITMQ_VHOST),
|
||||
};
|
||||
26
packages/sim-shared/config/eventBusConfig.ts
Normal file
26
packages/sim-shared/config/eventBusConfig.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { RabbitMQEventBus, RMQConnectionParams } from "sim-shared/infrastructure/RabbitMQEventBus.js"
|
||||
import { env } from "./env/index.js"
|
||||
|
||||
const rmqUser = env.RABBITMQ_USER
|
||||
const rmqPass = env.RABBITMQ_PASSWORD
|
||||
const rmqHost = env.RABBITMQ_HOST
|
||||
const rmqPort = Number(env.RABBITMQ_PORT)
|
||||
const rmqSecure = false
|
||||
const rmqVhost = env.RABBITMQ_VHOST
|
||||
|
||||
export const rmqConnOptions = <RMQConnectionParams>{
|
||||
username: rmqUser,
|
||||
password: rmqPass,
|
||||
vhost: rmqVhost,
|
||||
hostname: rmqHost,
|
||||
port: rmqPort,
|
||||
secure: rmqSecure,
|
||||
}
|
||||
|
||||
export const rabbitmqEventBus = new RabbitMQEventBus({
|
||||
connectionParams: rmqConnOptions
|
||||
})
|
||||
|
||||
export async function startRMQClient() {
|
||||
await rabbitmqEventBus.connect()
|
||||
}
|
||||
7
packages/sim-shared/domain/Queue.ts
Normal file
7
packages/sim-shared/domain/Queue.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface Queue {
|
||||
name: string;
|
||||
messages: number;
|
||||
ready: number;
|
||||
unacked: number;
|
||||
consumers: number;
|
||||
}
|
||||
37
packages/sim-shared/infrastructure/RabbitManagementClient.ts
Normal file
37
packages/sim-shared/infrastructure/RabbitManagementClient.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
import axios, { AxiosInstance } from "axios";
|
||||
import { Queue } from "sim-shared/domain/Queue.js";
|
||||
|
||||
export class RabbitManagementClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor(args: {
|
||||
baseURL: string;
|
||||
user: string;
|
||||
password: string;
|
||||
}) {
|
||||
this.client = axios.create({
|
||||
baseURL: args.baseURL,
|
||||
auth: {
|
||||
username: args.user,
|
||||
password: args.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async getQueues(): Promise<Queue[]> {
|
||||
try {
|
||||
const response = await this.client.get("/queues")
|
||||
return response.data.map((q: any) => ({
|
||||
name: q.name,
|
||||
messages: q.messages,
|
||||
ready: q.messages_ready,
|
||||
unacked: q.messages_unacknowledged,
|
||||
consumers: q.consumers
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error("[RabbitManagementClient] Error obteniendo colas", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Request, Response } from "express";
|
||||
import { DashboardUseCases } from "./Dashboard.usecases.js";
|
||||
import { QueueSummary } from "#domain/Dashboard.js";
|
||||
import { OrderTracking } from "packages/sim-shared/domain/Order.js";
|
||||
|
||||
export class DashboardController {
|
||||
constructor(private readonly useCases: DashboardUseCases) {}
|
||||
|
||||
public async getQueuesFragment(req: Request, res: Response) {
|
||||
const data = await this.useCases.getDashboardData()
|
||||
res.send(renderQueuesHtml(data.queues))
|
||||
}
|
||||
|
||||
public async getOrdersFragment(req: Request, res: Response) {
|
||||
const data = await this.useCases.getDashboardData()
|
||||
res.send(renderOrdersHtml(data.pendingOrders))
|
||||
}
|
||||
|
||||
public async getFullFragment(req: Request, res: Response) {
|
||||
const data = await this.useCases.getDashboardData();
|
||||
res.send(`
|
||||
${renderQueuesHtml(data.queues)}
|
||||
${renderOrdersHtml(data.pendingOrders)}
|
||||
<p class="updated-at">Actualizado: ${new Date(data.generatedAt).toLocaleTimeString("es-ES")}</p>
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
function queueBadgeClass(count: number): string {
|
||||
if (count === 0 ) return "badge--ok"
|
||||
if (count < 10) return "badge--warn"
|
||||
return "badge--error"
|
||||
}
|
||||
|
||||
function orderBadgeClass(status: string): string {
|
||||
if (status === 'finished' ) return "badge--ok"
|
||||
if (status == 'pending') return "badge--warn"
|
||||
return "badge--error"
|
||||
}
|
||||
|
||||
function renderQueuesHtml(queues: QueueSummary): string {
|
||||
const row = (label: string, q: typeof queues.main) => `
|
||||
<tr>
|
||||
<td>${label}</td>
|
||||
<td>${q.name}</td>
|
||||
<td><span class="badge ${queueBadgeClass(q.messages)}">${q.messages}</span></td>
|
||||
<td>${q.ready}</td>
|
||||
<td>${q.unacked}</td>
|
||||
<td>${q.consumers}</td>
|
||||
</tr>
|
||||
`
|
||||
return `
|
||||
<section id="queues-section">
|
||||
<h2>Estado de las colas</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tipo</th>
|
||||
<th>Cola</th>
|
||||
<th>Total</th>
|
||||
<th>Ready</th>
|
||||
<th>Unacked</th>
|
||||
<th>Consumers</th>
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${row("Principal", queues.main)}
|
||||
${row("Reintentos", queues.retry)}
|
||||
${row("Fallidos (DLX)", queues.dlx)}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
|
||||
function renderOrdersHtml(orders: OrderTracking<unknown>[]): string {
|
||||
const rows = orders.map(o => `
|
||||
<tr>
|
||||
<td>${o.id}</td>
|
||||
<td>${o.correlation_id}</td>
|
||||
<td>${o.order_type}</td>
|
||||
<td><span class="badge ${orderBadgeClass(o.status)}">${o.status}</span></td>
|
||||
<td>${o.retry_count ?? 0}</td>
|
||||
<td>${new Date(o.start_date).toLocaleString("es-ES")}</td>
|
||||
</tr>
|
||||
`
|
||||
).join("")
|
||||
|
||||
return `
|
||||
<section id="orders-section">
|
||||
<h2>Tareas pendientes (${orders.length})</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Correlation ID</th>
|
||||
<th>Tipo</th>
|
||||
<th>Estado</th>
|
||||
<th>Reintentos</th>
|
||||
<th>Inicio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
</section>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { RabbitManagementClient } from "packages/sim-shared/infrastructure/RabbitManagementClient.js";
|
||||
import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js";
|
||||
import { DashboardData, QueueSummary } from "#domain/Dashboard.js";
|
||||
import { Queue } from "packages/sim-shared/domain/Queue.js";
|
||||
import { env } from "#config/env/index.js";
|
||||
|
||||
const EMPTY_QUEUE: Queue = { name: "", messages: 0, ready: 0, unacked: 0, consumers: 0}
|
||||
|
||||
export class DashboardUseCases {
|
||||
constructor(
|
||||
private readonly rabbitClient: RabbitManagementClient,
|
||||
private readonly orderRepo: OrderRepository,
|
||||
) {}
|
||||
|
||||
public async getDashboardData(): Promise<DashboardData> {
|
||||
//si uno peta, no rompe al otro
|
||||
const [queuesResult, pendingResult] = await Promise.allSettled([
|
||||
this.rabbitClient.getQueues(),
|
||||
this.orderRepo.getPendingOrders({ limit: 100 }), //por poner un límite
|
||||
])
|
||||
|
||||
let queues: QueueSummary = { main: EMPTY_QUEUE, retry: EMPTY_QUEUE, dlx: EMPTY_QUEUE}
|
||||
|
||||
if (queuesResult.status === 'fulfilled') {
|
||||
const all = queuesResult.value
|
||||
const find = (name: string) => all.find(q => q.name === name) ?? {...EMPTY_QUEUE, name}
|
||||
queues = {
|
||||
main: find(env.QUEUE_MAIN!),
|
||||
retry: find(env.QUEUE_RETRY!),
|
||||
dlx: find(env.QUEUE_DLX!)
|
||||
}
|
||||
} else {
|
||||
console.error('[Dasboard] Error obteniendo colas: ', queuesResult.reason)
|
||||
}
|
||||
const pendingOrders = (pendingResult.status === "fulfilled" && !pendingResult.value.error)
|
||||
? pendingResult.value.data ?? []
|
||||
: [];
|
||||
if (pendingResult.status === 'rejected') {
|
||||
console.error('[Dashboard]Error obteniendo tareas: ', pendingResult.reason)
|
||||
}
|
||||
return {
|
||||
queues,
|
||||
pendingOrders,
|
||||
generatedAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
22
packages/sim-visualizador-tareas-back/config/env/index.ts
vendored
Normal file
22
packages/sim-visualizador-tareas-back/config/env/index.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import { loadEnvFile } from "node:process";
|
||||
import path from "node:path";
|
||||
|
||||
loadEnvFile(path.join("../../env"))
|
||||
|
||||
export const env = {
|
||||
API_PORT: parseInt(process.env.API_PORT ?? "3010"),
|
||||
POSTGRES_USER: process.env.POSTGRES_USER,
|
||||
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
|
||||
POSTGRES_PORT: process.env.POSTGRES_PORT,
|
||||
POSTGRES_HOST: process.env.POSTGRES_HOST,
|
||||
POSTGRES_DATABASE: process.env.POSTGRES_DATABASE,
|
||||
RABBITMQ_HOST: String(process.env.RABBITMQ_HOST),
|
||||
RABBITMQ_PORT: process.env.RABBITMQ_PORT,
|
||||
RABBITMQ_USER: String(process.env.RABBITMQ_USER),
|
||||
RABBITMQ_PASSWORD: String(process.env.RABBITMQ_PASSWORD),
|
||||
RABBITMQ_SECURE: process.env.RABBITMQ_SECURE,
|
||||
RABBITMQ_VHOST: process.env.RABBITMQ_VHOST,
|
||||
QUEUE_MAIN: process.env.QUEUE_MAIN,
|
||||
QUEUE_RETRY: process.env.QUEUE_RETRY,
|
||||
QUEUE_DLX: process.env.QUEUE_DLX
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Pool } from "pg";
|
||||
import { PgClient } from "packages/sim-shared/infrastructure/PgClient.js";
|
||||
import { env } from "./env/index.js";
|
||||
|
||||
export const pgPool = new Pool({
|
||||
user: env.POSTGRES_USER!,
|
||||
host: env.POSTGRES_HOST!,
|
||||
database: env.POSTGRES_DATABASE!,
|
||||
password: env.POSTGRES_PASSWORD!,
|
||||
port: Number(env.POSTGRES_PORT) || 5432
|
||||
})
|
||||
|
||||
export const postgresClient = new PgClient({ pool: pgPool })
|
||||
@@ -0,0 +1,8 @@
|
||||
import { RabbitManagementClient } from "packages/sim-shared/infrastructure/RabbitManagementClient.js";
|
||||
import { env } from "./env/index.js";
|
||||
|
||||
export const rabbitManagementClient = new RabbitManagementClient({
|
||||
baseURL: env.RABBITMQ_HOST!,
|
||||
user: env.RABBITMQ_USER!,
|
||||
password: env.RABBITMQ_PASSWORD!
|
||||
})
|
||||
14
packages/sim-visualizador-tareas-back/domain/Dashboard.ts
Normal file
14
packages/sim-visualizador-tareas-back/domain/Dashboard.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Queue } from "packages/sim-shared/domain/Queue.js";
|
||||
import { OrderTracking } from "packages/sim-shared/domain/Order.js";
|
||||
|
||||
export type QueueSummary = {
|
||||
main: Queue;
|
||||
retry: Queue;
|
||||
dlx: Queue;
|
||||
}
|
||||
|
||||
export interface DashboardData {
|
||||
queues: QueueSummary;
|
||||
pendingOrders: OrderTracking<unknown>[];
|
||||
generatedAt: string;
|
||||
}
|
||||
45
packages/sim-visualizador-tareas-back/index.ts
Normal file
45
packages/sim-visualizador-tareas-back/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import path from "path";
|
||||
import { env } from "#config/env/index.js";
|
||||
import { pgPool, postgresClient } from "#config/postgreConfig.js";
|
||||
import { OrderRepository } from "packages/sim-shared/infrastructure/OrderRepository.js";
|
||||
import { DashboardController } from "./aplication/Dashboard.controller.js";
|
||||
import { DashboardUseCases } from "./aplication/Dashboard.usecases.js";
|
||||
import { createDashboardRouter } from "#adapters/dashboardRoutes.http.js";
|
||||
import { PgClient } from "packages/sim-shared/infrastructure/PgClient.js";
|
||||
import { RabbitManagementClient } from "packages/sim-shared/infrastructure/RabbitManagementClient.js";
|
||||
|
||||
const pgClient = new PgClient({
|
||||
pool: pgPool,
|
||||
})
|
||||
|
||||
const rabbitClient = new RabbitManagementClient({
|
||||
baseURL: env.RABBITMQ_HOST,
|
||||
user: env.RABBITMQ_USER,
|
||||
password: env.RABBITMQ_PASSWORD,
|
||||
})
|
||||
|
||||
const orderRepo = new OrderRepository(pgClient)
|
||||
const useCases = new DashboardUseCases(rabbitClient, orderRepo)
|
||||
const controller = new DashboardController(useCases)
|
||||
const dashboardRouter = createDashboardRouter(controller)
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(process.cwd(), "../sim-visualizador-tareas-front")));
|
||||
app.use("/", dashboardRouter);
|
||||
app.get("/health", (_req, res) => res.status(200).json({ status: "ok" }));
|
||||
|
||||
async function main() {
|
||||
await pgClient.checkDatabaseConnection();
|
||||
app.listen(Number(env.API_PORT), "0.0.0.0", () => {
|
||||
console.log("[o] Visualizador iniciado en el puerto %d", env.API_PORT);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("[x] Error arrancando el visualizador:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { DashboardController } from "../aplication/Dashboard.controller.js";
|
||||
|
||||
export function createDashboardRouter(controller: DashboardController): Router {
|
||||
const router = Router()
|
||||
|
||||
router.get("/fragments/queues", (req, res) => controller.getQueuesFragment(req, res))
|
||||
router.get("/fragments/orders", (req, res) => controller.getOrdersFragment(req, res))
|
||||
router.get("/fragments/dashboard", (req, res) => controller.getFullFragment(req, res))
|
||||
|
||||
return router
|
||||
}
|
||||
72
packages/sim-visualizador-tareas-back/package.json
Normal file
72
packages/sim-visualizador-tareas-back/package.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "sim-visualizador-tareas-back",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"imports": {
|
||||
"#config/*.js": {
|
||||
"types": "./config/*.ts",
|
||||
"default": "./config/*.js"
|
||||
},
|
||||
"#config/*": {
|
||||
"types": "./config/*.ts",
|
||||
"default": "./config/*.js"
|
||||
},
|
||||
"#adapters/*.js": {
|
||||
"types": "./infrastructure/*.ts",
|
||||
"default": "./infrastructure/*.js"
|
||||
},
|
||||
"#adapters/*": {
|
||||
"types": "./infrastructure/*.ts",
|
||||
"default": "./infrastructure/*.js"
|
||||
},
|
||||
"#domain/*.js": {
|
||||
"types": "./domain/*.ts",
|
||||
"default": "./domain/*.js"
|
||||
},
|
||||
"#domain/*": {
|
||||
"types": "./domain/*.ts",
|
||||
"default": "./domain/*.js"
|
||||
},
|
||||
"#ports/*.js": {
|
||||
"types": "./ports/*.ts",
|
||||
"default": "./ports/*.js"
|
||||
},
|
||||
"#ports/*": {
|
||||
"types": "./ports/*.ts",
|
||||
"default": "./ports/*.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --import tsx --test ./**/*.test.ts",
|
||||
"build": "tsc --build && tsc-alias -p tsconfig.json && cp package.json ../../dist/packages/sim-visualizador-tareas-back/",
|
||||
"dev": "tsx watch index.ts",
|
||||
"start": "node ../../dist/packages/sim-visualizador-tareas-back/index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"dependencies": {
|
||||
"@tsconfig/node22": "*",
|
||||
"amqplib": "^0.10.9",
|
||||
"axios": "*",
|
||||
"cors": "*",
|
||||
"dotenv": "*",
|
||||
"express": "*",
|
||||
"sim-shared": "sim-shared:*",
|
||||
"typescript": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/amqplib": "^0.10.8",
|
||||
"@types/cors": "*",
|
||||
"@types/express": "*",
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "*",
|
||||
"prettier": "*",
|
||||
"supertest": "*",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsx": "*",
|
||||
"vitest": "*"
|
||||
}
|
||||
}
|
||||
17
packages/sim-visualizador-tareas-back/tsconfig.json
Normal file
17
packages/sim-visualizador-tareas-back/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist",
|
||||
"rootDir": "../../",
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../packages/sim-shared/**/*.ts"
|
||||
],
|
||||
"files": [
|
||||
"config/env/index.ts"
|
||||
]
|
||||
}
|
||||
0
packages/sim-visualizador-tareas-front/index.html
Normal file
0
packages/sim-visualizador-tareas-front/index.html
Normal file
8
sf-sim.code-workspace
Normal file
8
sf-sim.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
Reference in New Issue
Block a user