Feat: add prometheus compatible endpoint, seed fallback store and add env.example
This commit is contained in:
@@ -1,166 +1,184 @@
|
||||
const express = require('express');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
require('dotenv').config();
|
||||
const express = require("express");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
require("dotenv").config();
|
||||
|
||||
// Import utilities
|
||||
const redisClient = require('./src/utils/redis-client');
|
||||
const fallbackStore = require('./src/utils/fallback-store');
|
||||
const logger = require('./src/utils/logger');
|
||||
const pdfGenerator = require('./src/utils/pdf-generator');
|
||||
const redisClient = require("./src/utils/redis-client");
|
||||
const fallbackStore = require("./src/utils/fallback-store");
|
||||
const logger = require("./src/utils/logger");
|
||||
const pdfGenerator = require("./src/utils/pdf-generator");
|
||||
const metrics = require("./src/utils/metrics");
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3049;
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
app.use(express.static("public"));
|
||||
|
||||
// Request logging middleware
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
res.on("finish", () => {
|
||||
const responseTime = Date.now() - start;
|
||||
logger.logRequest(req, res, responseTime);
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Global error handler
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error('Unhandled error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Internal Server Error',
|
||||
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
||||
});
|
||||
});
|
||||
// Prometheus metrics middleware
|
||||
app.use(metrics.metricsMiddleware);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', async (req, res) => {
|
||||
app.get("/health", async (req, res) => {
|
||||
const redisHealthy = redisClient.isHealthy();
|
||||
const fallbackActive = fallbackStore.isActive;
|
||||
|
||||
|
||||
// Update metrics
|
||||
metrics.updateRedisStatus(redisHealthy);
|
||||
metrics.updateFallbackStatus(fallbackActive);
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
status: "ok",
|
||||
timestamp: new Date().toISOString(),
|
||||
redis: {
|
||||
connected: redisHealthy,
|
||||
fallbackActive: fallbackActive
|
||||
fallbackActive: fallbackActive,
|
||||
},
|
||||
uptime: process.uptime()
|
||||
uptime: process.uptime(),
|
||||
});
|
||||
});
|
||||
|
||||
// Get all events endpoint
|
||||
app.get('/events', async (req, res) => {
|
||||
app.get("/events", async (req, res) => {
|
||||
try {
|
||||
let events;
|
||||
|
||||
|
||||
if (redisClient.isHealthy()) {
|
||||
events = await redisClient.getAllEvents();
|
||||
} else {
|
||||
events = fallbackStore.getAllEvents();
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
events,
|
||||
usingFallback: fallbackStore.isActive
|
||||
usingFallback: fallbackStore.isActive,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching events:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch events'
|
||||
logger.error("Error fetching events:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to fetch events",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific event stats
|
||||
app.get('/events/:eventId', async (req, res) => {
|
||||
app.get("/events/:eventId", async (req, res) => {
|
||||
try {
|
||||
const eventId = req.params.eventId;
|
||||
let eventStats;
|
||||
|
||||
|
||||
if (redisClient.isHealthy()) {
|
||||
eventStats = await redisClient.getEventStats(eventId);
|
||||
} else {
|
||||
eventStats = fallbackStore.getEventStats(eventId);
|
||||
}
|
||||
|
||||
|
||||
if (!eventStats) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Event not found'
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "Event not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
event: eventStats,
|
||||
usingFallback: fallbackStore.isActive
|
||||
usingFallback: fallbackStore.isActive,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching event stats:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch event stats'
|
||||
logger.error("Error fetching event stats:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to fetch event stats",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Purchase ticket endpoint (multi-event)
|
||||
app.post('/buy/:eventId', async (req, res) => {
|
||||
app.post("/buy/:eventId", async (req, res) => {
|
||||
const startTime = Date.now();
|
||||
const eventId = req.params.eventId;
|
||||
const purchaseId = uuidv4();
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
|
||||
// Try Redis first
|
||||
if (redisClient.isHealthy()) {
|
||||
try {
|
||||
const luaResult = await redisClient.purchaseTicket(eventId, purchaseId, timestamp);
|
||||
|
||||
const luaResult = await redisClient.purchaseTicket(
|
||||
eventId,
|
||||
purchaseId,
|
||||
timestamp
|
||||
);
|
||||
|
||||
if (luaResult[0]) {
|
||||
// Success - generate PDF ticket
|
||||
try {
|
||||
// Get event details for PDF
|
||||
const eventStats = await redisClient.getEventStats(eventId);
|
||||
|
||||
|
||||
const pdfStartTime = Date.now();
|
||||
const pdfResult = await pdfGenerator.generateTicketPDF({
|
||||
ticketId: luaResult[0],
|
||||
eventId,
|
||||
purchaseId,
|
||||
eventName: eventStats?.name || `Event ${eventId}`,
|
||||
eventDescription: eventStats?.description || 'Event description not available',
|
||||
eventDescription:
|
||||
eventStats?.description || "Event description not available",
|
||||
timestamp,
|
||||
soldCount: luaResult[2]
|
||||
soldCount: luaResult[2],
|
||||
});
|
||||
|
||||
|
||||
// Record PDF generation metrics
|
||||
const pdfDuration = (Date.now() - pdfStartTime) / 1000;
|
||||
metrics.recordPDFGeneration(
|
||||
pdfResult.success ? "success" : "failed",
|
||||
pdfDuration
|
||||
);
|
||||
|
||||
result = {
|
||||
success: true,
|
||||
ticket: luaResult[0],
|
||||
purchaseId,
|
||||
eventId,
|
||||
soldCount: luaResult[2],
|
||||
message: 'Ticket purchased successfully!',
|
||||
message: "Ticket purchased successfully!",
|
||||
usingFallback: false,
|
||||
pdf: {
|
||||
generated: pdfResult.success,
|
||||
filename: pdfResult.filename,
|
||||
downloadUrl: `/tickets/${purchaseId}`
|
||||
}
|
||||
downloadUrl: `/tickets/${purchaseId}`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Record metrics for successful purchase
|
||||
metrics.recordTicketSale(eventId, "success");
|
||||
metrics.updateTicketMetrics(
|
||||
eventId,
|
||||
luaResult[2],
|
||||
luaResult[3] || 0
|
||||
);
|
||||
|
||||
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
||||
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
|
||||
|
||||
} catch (pdfError) {
|
||||
logger.error('PDF generation failed:', pdfError);
|
||||
|
||||
logger.error("PDF generation failed:", pdfError);
|
||||
|
||||
// Still return success for ticket purchase, but note PDF failure
|
||||
result = {
|
||||
success: true,
|
||||
@@ -168,83 +186,106 @@ app.post('/buy/:eventId', async (req, res) => {
|
||||
purchaseId,
|
||||
eventId,
|
||||
soldCount: luaResult[2],
|
||||
message: 'Ticket purchased successfully! (PDF generation failed)',
|
||||
message: "Ticket purchased successfully! (PDF generation failed)",
|
||||
usingFallback: false,
|
||||
pdf: {
|
||||
generated: false,
|
||||
error: 'PDF generation failed'
|
||||
}
|
||||
error: "PDF generation failed",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
// Record metrics for successful purchase (even with PDF failure)
|
||||
metrics.recordTicketSale(eventId, "success");
|
||||
metrics.updateTicketMetrics(
|
||||
eventId,
|
||||
luaResult[2],
|
||||
luaResult[3] || 0
|
||||
);
|
||||
|
||||
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
||||
}
|
||||
} else {
|
||||
// Failed - handle specific error
|
||||
const errorCode = luaResult[1];
|
||||
let statusCode = 400;
|
||||
let message = 'Purchase failed';
|
||||
|
||||
let message = "Purchase failed";
|
||||
|
||||
switch (errorCode) {
|
||||
case 'EVENT_NOT_FOUND':
|
||||
case "EVENT_NOT_FOUND":
|
||||
statusCode = 404;
|
||||
message = 'Event not found';
|
||||
message = "Event not found";
|
||||
break;
|
||||
case 'NO_TICKETS_AVAILABLE':
|
||||
case "NO_TICKETS_AVAILABLE":
|
||||
statusCode = 409;
|
||||
message = 'No tickets available for this event';
|
||||
message = "No tickets available for this event";
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Record metrics for failed purchase
|
||||
metrics.recordTicketSale(eventId, "failed");
|
||||
|
||||
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
return res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
errorCode,
|
||||
eventId,
|
||||
purchaseId
|
||||
purchaseId,
|
||||
});
|
||||
}
|
||||
} catch (redisError) {
|
||||
logger.error('Redis purchase failed, attempting fallback:', redisError);
|
||||
logger.error("Redis purchase failed, attempting fallback:", redisError);
|
||||
// Activate fallback if not already active
|
||||
if (!fallbackStore.isActive) {
|
||||
fallbackStore.activate('Redis purchase operation failed');
|
||||
fallbackStore.activate("Redis purchase operation failed");
|
||||
// Try to sync with Redis data if possible
|
||||
setTimeout(() => {
|
||||
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
|
||||
fallbackStore.attemptReseed();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
throw redisError; // Will be caught by outer try-catch for fallback
|
||||
}
|
||||
} else {
|
||||
throw new Error('Redis not available');
|
||||
throw new Error("Redis not available");
|
||||
}
|
||||
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
result.responseTime = `${responseTime}ms`;
|
||||
|
||||
|
||||
res.json(result);
|
||||
|
||||
} catch (error) {
|
||||
// Fallback to in-memory store
|
||||
try {
|
||||
if (!fallbackStore.isActive) {
|
||||
fallbackStore.activate('Redis connection failed during purchase');
|
||||
fallbackStore.activate("Redis connection failed during purchase");
|
||||
// Try to sync with Redis data if possible
|
||||
setTimeout(() => {
|
||||
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
|
||||
fallbackStore.attemptReseed();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
|
||||
const fallbackResult = fallbackStore.purchaseTicket(eventId, purchaseId);
|
||||
|
||||
|
||||
if (fallbackResult.success) {
|
||||
// Generate PDF for fallback purchase
|
||||
try {
|
||||
const eventStats = fallbackStore.getEventStats(eventId);
|
||||
|
||||
|
||||
const pdfResult = await pdfGenerator.generateTicketPDF({
|
||||
ticketId: fallbackResult.ticket,
|
||||
eventId,
|
||||
purchaseId,
|
||||
eventName: eventStats?.name || `Event ${eventId}`,
|
||||
eventDescription: eventStats?.description || 'Event description not available',
|
||||
eventDescription:
|
||||
eventStats?.description || "Event description not available",
|
||||
timestamp,
|
||||
soldCount: fallbackResult.soldCount
|
||||
soldCount: fallbackResult.soldCount,
|
||||
});
|
||||
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -252,21 +293,30 @@ app.post('/buy/:eventId', async (req, res) => {
|
||||
purchaseId,
|
||||
eventId,
|
||||
soldCount: fallbackResult.soldCount,
|
||||
message: 'Ticket purchased successfully (fallback mode)!',
|
||||
message: "Ticket purchased successfully (fallback mode)!",
|
||||
usingFallback: true,
|
||||
responseTime: `${responseTime}ms`,
|
||||
pdf: {
|
||||
generated: pdfResult.success,
|
||||
filename: pdfResult.filename,
|
||||
downloadUrl: `/tickets/${purchaseId}`
|
||||
}
|
||||
downloadUrl: `/tickets/${purchaseId}`,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(`PDF ticket generated for fallback purchase ${purchaseId}`);
|
||||
|
||||
|
||||
// Record metrics for successful fallback purchase
|
||||
metrics.recordTicketSale(eventId, "success_fallback");
|
||||
metrics.updateTicketMetrics(
|
||||
eventId,
|
||||
fallbackResult.soldCount,
|
||||
fallbackResult.remainingTickets || 0
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`PDF ticket generated for fallback purchase ${purchaseId}`
|
||||
);
|
||||
} catch (pdfError) {
|
||||
logger.error('PDF generation failed in fallback mode:', pdfError);
|
||||
|
||||
logger.error("PDF generation failed in fallback mode:", pdfError);
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -274,128 +324,218 @@ app.post('/buy/:eventId', async (req, res) => {
|
||||
purchaseId,
|
||||
eventId,
|
||||
soldCount: fallbackResult.soldCount,
|
||||
message: 'Ticket purchased successfully (fallback mode, PDF generation failed)!',
|
||||
message:
|
||||
"Ticket purchased successfully (fallback mode, PDF generation failed)!",
|
||||
usingFallback: true,
|
||||
responseTime: `${responseTime}ms`,
|
||||
pdf: {
|
||||
generated: false,
|
||||
error: 'PDF generation failed'
|
||||
}
|
||||
error: "PDF generation failed",
|
||||
},
|
||||
});
|
||||
|
||||
// Record metrics for successful fallback purchase (even with PDF failure)
|
||||
metrics.recordTicketSale(eventId, "success_fallback");
|
||||
metrics.updateTicketMetrics(
|
||||
eventId,
|
||||
fallbackResult.soldCount,
|
||||
fallbackResult.remainingTickets || 0
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let statusCode = 400;
|
||||
let message = 'Purchase failed';
|
||||
|
||||
let message = "Purchase failed";
|
||||
|
||||
switch (fallbackResult.error) {
|
||||
case 'EVENT_NOT_FOUND':
|
||||
case "EVENT_NOT_FOUND":
|
||||
statusCode = 404;
|
||||
message = 'Event not found';
|
||||
message = "Event not found";
|
||||
break;
|
||||
case 'NO_TICKETS_AVAILABLE':
|
||||
case "NO_TICKETS_AVAILABLE":
|
||||
statusCode = 409;
|
||||
message = 'No tickets available for this event';
|
||||
message = "No tickets available for this event";
|
||||
break;
|
||||
}
|
||||
|
||||
logger.logPurchase(eventId, null, purchaseId, false, fallbackResult.error);
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
|
||||
// Record metrics for failed fallback purchase
|
||||
metrics.recordTicketSale(eventId, "failed_fallback");
|
||||
|
||||
logger.logPurchase(
|
||||
eventId,
|
||||
null,
|
||||
purchaseId,
|
||||
false,
|
||||
fallbackResult.error
|
||||
);
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
message,
|
||||
errorCode: fallbackResult.error,
|
||||
eventId,
|
||||
purchaseId,
|
||||
usingFallback: true
|
||||
usingFallback: true,
|
||||
});
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
logger.error('Both Redis and fallback failed:', fallbackError);
|
||||
logger.error("Both Redis and fallback failed:", fallbackError);
|
||||
|
||||
// Record metrics for system failure
|
||||
metrics.recordTicketSale(eventId, "system_error");
|
||||
|
||||
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'System temporarily unavailable',
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "System temporarily unavailable",
|
||||
eventId,
|
||||
purchaseId
|
||||
purchaseId,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Download ticket PDF endpoint
|
||||
app.get('/tickets/:purchaseId', async (req, res) => {
|
||||
app.get("/tickets/:purchaseId", async (req, res) => {
|
||||
try {
|
||||
const purchaseId = req.params.purchaseId;
|
||||
|
||||
|
||||
if (!pdfGenerator.ticketExists(purchaseId)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Ticket not found'
|
||||
message: "Ticket not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const filepath = pdfGenerator.getTicketPath(purchaseId);
|
||||
const filename = `ticket-${purchaseId}.pdf`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
const fileStream = require('fs').createReadStream(filepath);
|
||||
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||
|
||||
const fileStream = require("fs").createReadStream(filepath);
|
||||
fileStream.pipe(res);
|
||||
|
||||
|
||||
logger.info(`PDF ticket downloaded: ${purchaseId}`);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error downloading ticket:', error);
|
||||
logger.error("Error downloading ticket:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to download ticket'
|
||||
message: "Failed to download ticket",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// PDF management endpoint
|
||||
app.get('/admin/pdf-stats', async (req, res) => {
|
||||
app.get("/admin/pdf-stats", async (req, res) => {
|
||||
try {
|
||||
const stats = pdfGenerator.getStats();
|
||||
res.json({
|
||||
success: true,
|
||||
stats
|
||||
stats,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting PDF stats:', error);
|
||||
logger.error("Error getting PDF stats:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to get PDF statistics'
|
||||
message: "Failed to get PDF statistics",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup old tickets endpoint
|
||||
app.post('/admin/cleanup-tickets', async (req, res) => {
|
||||
app.post("/admin/cleanup-tickets", async (req, res) => {
|
||||
try {
|
||||
const maxAgeHours = req.body.maxAgeHours || 24;
|
||||
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
|
||||
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Cleaned up ${deletedCount} old tickets`,
|
||||
deletedCount
|
||||
deletedCount,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error cleaning up tickets:', error);
|
||||
logger.error("Error cleaning up tickets:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to cleanup tickets'
|
||||
message: "Failed to cleanup tickets",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Seed fallback store endpoint
|
||||
app.post("/admin/seed-fallback", async (req, res) => {
|
||||
try {
|
||||
if (redisClient.isHealthy()) {
|
||||
// Activate fallback store temporarily for seeding
|
||||
fallbackStore.activate("Manual seeding from admin endpoint");
|
||||
|
||||
// Get all events from Redis and seed fallback store
|
||||
const events = await redisClient.getAllEvents();
|
||||
const globalStats = await redisClient.getGlobalStats();
|
||||
|
||||
for (const event of events) {
|
||||
// Get remaining tickets for this event
|
||||
const remainingTickets = await redisClient.getRemainingTickets(
|
||||
event.eventId
|
||||
);
|
||||
|
||||
// Create metadata object
|
||||
const metadata = {
|
||||
eventId: event.eventId,
|
||||
totalTickets: event.totalTickets,
|
||||
soldTickets: event.soldTickets,
|
||||
createdAt: event.createdAt,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
lastSoldAt: event.lastSoldAt,
|
||||
};
|
||||
|
||||
// Seed the event in fallback store
|
||||
fallbackStore.seedEvent(event.eventId, remainingTickets, metadata);
|
||||
|
||||
// Update sold tickets count
|
||||
const fallbackEvent = fallbackStore.events.get(event.eventId);
|
||||
if (fallbackEvent) {
|
||||
fallbackEvent.soldTickets = event.soldTickets;
|
||||
}
|
||||
}
|
||||
|
||||
// Update global stats
|
||||
if (globalStats) {
|
||||
fallbackStore.globalStats.totalSold = globalStats.totalSold;
|
||||
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
|
||||
}
|
||||
|
||||
// Deactivate fallback store (will be activated when needed)
|
||||
fallbackStore.deactivate();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Fallback store seeded with ${events.length} events`,
|
||||
eventsCount: events.length,
|
||||
totalTickets: globalStats?.totalTickets || 0,
|
||||
totalSold: globalStats?.totalSold || 0,
|
||||
});
|
||||
} else {
|
||||
res.status(503).json({
|
||||
success: false,
|
||||
message: "Redis not available - cannot seed fallback store",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error seeding fallback store:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to seed fallback store",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Metrics endpoint (Prometheus compatible)
|
||||
app.get('/metrics', async (req, res) => {
|
||||
app.get("/metrics", async (req, res) => {
|
||||
try {
|
||||
let globalStats, events;
|
||||
|
||||
|
||||
if (redisClient.isHealthy()) {
|
||||
globalStats = await redisClient.getGlobalStats();
|
||||
events = await redisClient.getAllEvents();
|
||||
@@ -403,10 +543,10 @@ app.get('/metrics', async (req, res) => {
|
||||
globalStats = fallbackStore.getGlobalStats();
|
||||
events = fallbackStore.getAllEvents();
|
||||
}
|
||||
|
||||
|
||||
// Get PDF stats
|
||||
const pdfStats = pdfGenerator.getStats();
|
||||
|
||||
|
||||
// Calculate metrics
|
||||
const metrics = {
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -416,17 +556,17 @@ app.get('/metrics', async (req, res) => {
|
||||
usingFallback: fallbackStore.isActive,
|
||||
redisConnected: redisClient.isHealthy(),
|
||||
uptime: process.uptime(),
|
||||
memoryUsage: process.memoryUsage()
|
||||
memoryUsage: process.memoryUsage(),
|
||||
},
|
||||
pdf: pdfStats
|
||||
pdf: pdfStats,
|
||||
};
|
||||
|
||||
|
||||
res.json(metrics);
|
||||
} catch (error) {
|
||||
logger.error('Error generating metrics:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to generate metrics'
|
||||
logger.error("Error generating metrics:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Failed to generate metrics",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -436,8 +576,11 @@ async function initializeServer() {
|
||||
try {
|
||||
// Connect to Redis
|
||||
await redisClient.connect();
|
||||
logger.info('Redis connected successfully');
|
||||
|
||||
logger.info("Redis connected successfully");
|
||||
|
||||
// Ensure fallback store is seeded with current Redis data
|
||||
await ensureFallbackStoreSeeded();
|
||||
|
||||
// Start server
|
||||
app.listen(port, () => {
|
||||
logger.info(`🚀 Ticket Microservice running on port ${port}`);
|
||||
@@ -445,39 +588,108 @@ async function initializeServer() {
|
||||
logger.info(`📈 Metrics: http://localhost:${port}/metrics`);
|
||||
logger.info(`🎫 Events: http://localhost:${port}/events`);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize server:', error);
|
||||
logger.warn('Starting server with fallback store only');
|
||||
|
||||
fallbackStore.activate('Redis connection failed at startup');
|
||||
|
||||
logger.error("Failed to initialize server:", error);
|
||||
logger.warn("Starting server with fallback store only");
|
||||
|
||||
fallbackStore.activate("Redis connection failed at startup");
|
||||
|
||||
app.listen(port, () => {
|
||||
logger.warn(`⚠️ Server running in FALLBACK MODE on port ${port}`);
|
||||
logger.warn('Redis connection failed - using in-memory store');
|
||||
logger.warn("Redis connection failed - using in-memory store");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure fallback store is seeded with current Redis data
|
||||
async function ensureFallbackStoreSeeded() {
|
||||
try {
|
||||
// Check if fallback store needs seeding
|
||||
if (fallbackStore.events.size === 0) {
|
||||
logger.info(
|
||||
"Fallback store is empty, seeding with current Redis data..."
|
||||
);
|
||||
|
||||
// Temporarily activate fallback store for seeding
|
||||
fallbackStore.activate("Seeding during server initialization");
|
||||
|
||||
// Get all events from Redis and seed fallback store
|
||||
const events = await redisClient.getAllEvents();
|
||||
const globalStats = await redisClient.getGlobalStats();
|
||||
|
||||
for (const event of events) {
|
||||
// Get remaining tickets for this event
|
||||
const remainingTickets = await redisClient.getRemainingTickets(
|
||||
event.eventId
|
||||
);
|
||||
|
||||
// Create metadata object
|
||||
const metadata = {
|
||||
eventId: event.eventId,
|
||||
totalTickets: event.totalTickets,
|
||||
soldTickets: event.soldTickets,
|
||||
createdAt: event.createdAt,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
lastSoldAt: event.lastSoldAt,
|
||||
};
|
||||
|
||||
// Seed the event in fallback store
|
||||
fallbackStore.seedEvent(event.eventId, remainingTickets, metadata);
|
||||
|
||||
// Update sold tickets count
|
||||
const fallbackEvent = fallbackStore.events.get(event.eventId);
|
||||
if (fallbackEvent) {
|
||||
fallbackEvent.soldTickets = event.soldTickets;
|
||||
}
|
||||
}
|
||||
|
||||
// Update global stats
|
||||
if (globalStats) {
|
||||
fallbackStore.globalStats.totalSold = globalStats.totalSold;
|
||||
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
|
||||
}
|
||||
|
||||
logger.info(`Fallback store seeded with ${events.length} events`);
|
||||
|
||||
// Deactivate fallback store (will be activated when needed)
|
||||
fallbackStore.deactivate();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error seeding fallback store during initialization:", error);
|
||||
// Don't fail server startup if fallback seeding fails
|
||||
}
|
||||
}
|
||||
|
||||
// Global error handler
|
||||
app.use((err, req, res, next) => {
|
||||
logger.error("Unhandled error:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: "Internal Server Error",
|
||||
error: process.env.NODE_ENV === "development" ? err.message : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('Received SIGINT, shutting down gracefully...');
|
||||
process.on("SIGINT", async () => {
|
||||
logger.info("Received SIGINT, shutting down gracefully...");
|
||||
try {
|
||||
await redisClient.disconnect();
|
||||
logger.info('Redis disconnected');
|
||||
logger.info("Redis disconnected");
|
||||
} catch (error) {
|
||||
logger.error('Error disconnecting Redis:', error);
|
||||
logger.error("Error disconnecting Redis:", error);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
logger.info('Received SIGTERM, shutting down gracefully...');
|
||||
process.on("SIGTERM", async () => {
|
||||
logger.info("Received SIGTERM, shutting down gracefully...");
|
||||
try {
|
||||
await redisClient.disconnect();
|
||||
logger.info('Redis disconnected');
|
||||
logger.info("Redis disconnected");
|
||||
} catch (error) {
|
||||
logger.error('Error disconnecting Redis:', error);
|
||||
logger.error("Error disconnecting Redis:", error);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user