testapp: enhance logging and error handling across services and controllers

This commit is contained in:
Carlos Santos 2025-07-24 18:49:31 +02:00
parent 75fbeea807
commit 160cb3927d
6 changed files with 301 additions and 52 deletions

View File

@ -140,6 +140,20 @@ jobs:
env: env:
RUN_MODE: CI RUN_MODE: CI
PLAYWRIGHT_BROWSERS_PATH: /tmp/ms-playwright PLAYWRIGHT_BROWSERS_PATH: /tmp/ms-playwright
- name: Collect TestApp logs
if: always()
run: |
echo "=== Collecting TestApp logs ==="
if [ -f testapp/testapp.log ]; then
echo "TestApp log file found, copying..."
cp testapp/testapp.log testapp-logs-webhooks.log
else
echo "TestApp log file not found at testapp/testapp.log"
echo "Looking for log files..."
find . -name "testapp.log" -type f 2>/dev/null || echo "No testapp.log files found"
echo "Creating empty log file for artifact upload"
echo "TestApp log file not found during CI run" > testapp-logs-webhooks.log
fi
- name: Upload failed test videos - name: Upload failed test videos
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -148,6 +162,13 @@ jobs:
path: | path: |
frontend/webcomponent/test-results/ frontend/webcomponent/test-results/
retention-days: 2 retention-days: 2
- name: Upload TestApp logs
if: always()
uses: actions/upload-artifact@v4
with:
name: testapp-logs-webhooks-functionality
path: testapp-logs-webhooks.log
retention-days: 2
- name: Clean up - name: Clean up
if: always() if: always()
uses: OpenVidu/actions/cleanup@main uses: OpenVidu/actions/cleanup@main
@ -190,6 +211,20 @@ jobs:
env: env:
RUN_MODE: CI RUN_MODE: CI
PLAYWRIGHT_BROWSERS_PATH: /tmp/ms-playwright PLAYWRIGHT_BROWSERS_PATH: /tmp/ms-playwright
- name: Collect TestApp logs
if: always()
run: |
echo "=== Collecting TestApp logs ==="
if [ -f testapp/testapp.log ]; then
echo "TestApp log file found, copying..."
cp testapp/testapp.log testapp-logs-ui-features.log
else
echo "TestApp log file not found at testapp/testapp.log"
echo "Looking for log files..."
find . -name "testapp.log" -type f 2>/dev/null || echo "No testapp.log files found"
echo "Creating empty log file for artifact upload"
echo "TestApp log file not found during CI run" > testapp-logs-ui-features.log
fi
- name: Upload failed test videos - name: Upload failed test videos
if: always() if: always()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@ -198,6 +233,13 @@ jobs:
path: | path: |
frontend/webcomponent/test-results/ frontend/webcomponent/test-results/
retention-days: 2 retention-days: 2
- name: Upload TestApp logs
if: always()
uses: actions/upload-artifact@v4
with:
name: testapp-logs-ui-features
path: testapp-logs-ui-features.log
retention-days: 2
- name: Clean up - name: Clean up
if: always() if: always()
uses: OpenVidu/actions/cleanup@main uses: OpenVidu/actions/cleanup@main
@ -240,6 +282,20 @@ jobs:
env: env:
RUN_MODE: CI RUN_MODE: CI
PLAYWRIGHT_BROWSERS_PATH: /tmp/ms-playwright PLAYWRIGHT_BROWSERS_PATH: /tmp/ms-playwright
- name: Collect TestApp logs
if: always()
run: |
echo "=== Collecting TestApp logs ==="
if [ -f testapp/testapp.log ]; then
echo "TestApp log file found, copying..."
cp testapp/testapp.log testapp-logs-recording-access.log
else
echo "TestApp log file not found at testapp/testapp.log"
echo "Looking for log files..."
find . -name "testapp.log" -type f 2>/dev/null || echo "No testapp.log files found"
echo "Creating empty log file for artifact upload"
echo "TestApp log file not found during CI run" > testapp-logs-recording-access.log
fi
- name: Dump TestApp logs on failure - name: Dump TestApp logs on failure
if: failure() if: failure()
run: | run: |
@ -256,6 +312,13 @@ jobs:
path: | path: |
frontend/webcomponent/test-results/ frontend/webcomponent/test-results/
retention-days: 2 retention-days: 2
- name: Upload TestApp logs
if: always()
uses: actions/upload-artifact@v4
with:
name: testapp-logs-recording-access
path: testapp-logs-recording-access.log
retention-days: 2
- name: Dump OpenVidu Local Deployment logs - name: Dump OpenVidu Local Deployment logs
if: always() if: always()
shell: bash shell: bash

View File

@ -4,6 +4,9 @@ import { deleteAllRecordings, getAllRecordings } from '../services/recordingServ
export const getHome = async (_req: Request, res: Response) => { export const getHome = async (_req: Request, res: Response) => {
try { try {
console.log('Fetching rooms from:', `${process.env.MEET_API_URL || 'http://localhost:6080/api/v1'}/rooms`);
console.log('Using API key:', process.env.MEET_API_KEY || 'meet-api-key');
const { rooms } = await getAllRooms(); const { rooms } = await getAllRooms();
// Sort rooms by newest first // Sort rooms by newest first
@ -13,44 +16,78 @@ export const getHome = async (_req: Request, res: Response) => {
return dateB.getTime() - dateA.getTime(); return dateB.getTime() - dateA.getTime();
}); });
console.log(`Rooms fetched: ${rooms.length}`); console.log(`Rooms fetched successfully: ${rooms.length}`);
res.render('index', { rooms }); res.render('index', { rooms });
} catch (error) { } catch (error) {
console.error('Error fetching rooms:', error); console.error('Error fetching rooms:', error);
res.status(500).send('Internal Server Error'); console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace',
apiUrl: process.env.MEET_API_URL || 'http://localhost:6080/api/v1',
apiKey: process.env.MEET_API_KEY || 'meet-api-key'
});
res.status(500).send('Internal Server Error - Failed to fetch rooms: ' + (error instanceof Error ? error.message : 'Unknown error'));
return; return;
} }
}; };
export const postCreateRoom = async (req: Request, res: Response) => { export const postCreateRoom = async (req: Request, res: Response) => {
try { try {
console.log('Creating room with body:', JSON.stringify(req.body, null, 2));
const { roomIdPrefix, autoDeletionDate } = req.body; const { roomIdPrefix, autoDeletionDate } = req.body;
const preferences = processFormPreferences(req.body); const preferences = processFormPreferences(req.body);
await createRoom({ roomIdPrefix, autoDeletionDate, preferences }); console.log('Processed preferences:', JSON.stringify(preferences, null, 2));
console.log('Room creation parameters:', { roomIdPrefix, autoDeletionDate });
const result = await createRoom({ roomIdPrefix, autoDeletionDate, preferences });
console.log('Room created successfully:', result);
res.redirect('/'); res.redirect('/');
} catch (error) { } catch (error) {
console.error('Error creating room:', error); console.error('Error creating room:', error);
res.status(500).send('Internal Server Error'); console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace',
requestBody: req.body
});
res.status(500).send('Internal Server Error - Failed to create room: ' + (error instanceof Error ? error.message : 'Unknown error'));
return; return;
} }
}; };
export const deleteRoomCtrl = async (req: Request, res: Response) => { export const deleteRoomCtrl = async (req: Request, res: Response) => {
try { try {
console.log('Deleting room with body:', JSON.stringify(req.body, null, 2));
const { roomId } = req.body; const { roomId } = req.body;
if (!roomId) {
console.error('No roomId provided for deletion');
res.status(400).send('Bad Request - roomId is required');
return;
}
console.log(`Attempting to delete room: ${roomId}`);
await deleteRoom(roomId); await deleteRoom(roomId);
console.log(`Room ${roomId} deleted successfully`);
res.redirect('/'); res.redirect('/');
} catch (error) { } catch (error) {
console.error('Error deleting room:', error); console.error('Error deleting room:', error);
res.status(500).send('Internal Server Error'); console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace',
requestBody: req.body
});
res.status(500).send('Internal Server Error - Failed to delete room: ' + (error instanceof Error ? error.message : 'Unknown error'));
return; return;
} }
}; };
export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => { export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
try { try {
console.log('Deleting all rooms...');
const allRooms = await getAllRooms(); const allRooms = await getAllRooms();
console.log(`Found ${allRooms.rooms.length} rooms to delete`);
if (allRooms.rooms.length === 0) { if (allRooms.rooms.length === 0) {
console.log('No rooms to delete'); console.log('No rooms to delete');
res.render('index', { rooms: [] }); res.render('index', { rooms: [] });
@ -58,32 +95,45 @@ export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
} }
const roomIds = allRooms.rooms.map((room) => room.roomId); const roomIds = allRooms.rooms.map((room) => room.roomId);
console.log(`Deleting ${roomIds.length} rooms`, roomIds); console.log(`Deleting ${roomIds.length} rooms:`, roomIds);
await deleteAllRooms(roomIds); await deleteAllRooms(roomIds);
console.log('All rooms deleted successfully');
res.render('index', { rooms: [] }); res.render('index', { rooms: [] });
} catch (error) { } catch (error) {
console.error('Error deleting all rooms:', error); console.error('Error deleting all rooms:', error);
res.status(500).send('Internal Server Error ' + JSON.stringify(error)); console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace'
});
res.status(500).send('Internal Server Error - Failed to delete all rooms: ' + (error instanceof Error ? error.message : 'Unknown error'));
return; return;
} }
}; };
export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => { export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => {
try { try {
console.log('Deleting all recordings...');
const [{ recordings }, { rooms }] = await Promise.all([getAllRecordings(), getAllRooms()]); const [{ recordings }, { rooms }] = await Promise.all([getAllRecordings(), getAllRooms()]);
console.log(`Found ${recordings.length} recordings to delete`);
if (recordings.length === 0) { if (recordings.length === 0) {
console.log('No recordings to delete'); console.log('No recordings to delete');
res.render('index', { rooms }); res.render('index', { rooms });
return; return;
} }
const recordingIds = recordings.map((recording) => recording.recordingId); const recordingIds = recordings.map((recording) => recording.recordingId);
console.log(`Deleting ${recordingIds.length} recordings:`, recordingIds);
await deleteAllRecordings(recordingIds); await deleteAllRecordings(recordingIds);
console.log(`Deleted ${recordingIds.length} recordings`); console.log('All recordings deleted successfully');
res.render('index', { rooms }); res.render('index', { rooms });
} catch (error) { } catch (error) {
console.error('Error deleting all recordings:', error); console.error('Error deleting all recordings:', error);
res.status(500).send('Internal Server Error ' + JSON.stringify(error)); console.error('Error details:', {
message: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : 'No stack trace'
});
res.status(500).send('Internal Server Error - Failed to delete all recordings: ' + (error instanceof Error ? error.message : 'Unknown error'));
return; return;
} }
}; };

View File

@ -1,6 +1,7 @@
import express from 'express'; import express from 'express';
import http from 'http'; import http from 'http';
import path from 'path'; import path from 'path';
import fs from 'fs';
import { Server as IOServer } from 'socket.io'; import { Server as IOServer } from 'socket.io';
import { import {
deleteAllRecordingsCtrl, deleteAllRecordingsCtrl,
@ -12,6 +13,43 @@ import {
import { handleWebhook, joinRoom } from './controllers/roomController'; import { handleWebhook, joinRoom } from './controllers/roomController';
import { configService } from './services/configService'; import { configService } from './services/configService';
// Setup log file for CI debugging
const logStream = fs.createWriteStream(path.join(__dirname, '../../testapp.log'), { flags: 'a' });
// Override console methods to also write to log file
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
console.log = (...args: any[]) => {
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' ');
const timestamp = new Date().toISOString();
logStream.write(`[${timestamp}] LOG: ${message}\n`);
originalConsoleLog.apply(console, args);
};
console.error = (...args: any[]) => {
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' ');
const timestamp = new Date().toISOString();
logStream.write(`[${timestamp}] ERROR: ${message}\n`);
originalConsoleError.apply(console, args);
};
console.warn = (...args: any[]) => {
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' ');
const timestamp = new Date().toISOString();
logStream.write(`[${timestamp}] WARN: ${message}\n`);
originalConsoleWarn.apply(console, args);
};
console.log('=== TESTAPP STARTUP ===');
console.log('Testapp initializing at:', new Date().toISOString());
console.log('Environment variables:');
console.log('MEET_API_URL:', process.env.MEET_API_URL);
console.log('MEET_API_KEY:', process.env.MEET_API_KEY ? '***SET***' : 'NOT SET');
console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('PORT:', process.env.PORT);
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = new IOServer(server); const io = new IOServer(server);
@ -46,10 +84,36 @@ server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);
console.log(`Visit http://localhost:${PORT}/ to access the app`); console.log(`Visit http://localhost:${PORT}/ to access the app`);
console.log('-----------------------------------------'); console.log('-----------------------------------------');
console.log('Configuration:');
console.log(` MEET_API_URL: ${configService.meetApiUrl}`);
console.log(` MEET_API_KEY: ${configService.meetApiKey ? '[SET]' : '[NOT SET]'}`);
console.log(` MEET_WEBCOMPONENT_SRC: ${configService.meetWebhookSrc}`);
console.log(` SERVER_PORT: ${configService.serverPort}`);
console.log('-----------------------------------------');
console.log(''); console.log('');
console.log('OpenVidu Meet Configuration:'); console.log('OpenVidu Meet Configuration:');
console.log(`Meet API URL: ${configService.meetApiUrl}`); console.log(`Meet API URL: ${configService.meetApiUrl}`);
console.log(`Meet API key: ${configService.meetApiKey}`); console.log(`Meet API key: ${configService.meetApiKey}`);
console.log(`Meet Webcomponent Source: ${configService.meetWebhookSrc}`); console.log(`Meet Webcomponent Source: ${configService.meetWebhookSrc}`);
console.log('=== TESTAPP STARTUP COMPLETE ===');
});
// Handle graceful shutdown
process.on('SIGTERM', () => {
console.log('=== TESTAPP SHUTDOWN ===');
console.log('Received SIGTERM, shutting down gracefully');
logStream.end();
server.close(() => {
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('=== TESTAPP SHUTDOWN ===');
console.log('Received SIGINT, shutting down gracefully');
logStream.end();
server.close(() => {
process.exit(0);
});
}); });

View File

@ -8,32 +8,52 @@ export const getAllRecordings = async (): Promise<{
recordings: MeetRecordingInfo[]; recordings: MeetRecordingInfo[];
}> => { }> => {
const url = `${configService.meetApiUrl}/recordings`; const url = `${configService.meetApiUrl}/recordings`;
console.log(`Fetching all recordings from: ${url}`);
let { pagination, recordings } = await get<{ try {
pagination: any; let { pagination, recordings } = await get<{
recordings: MeetRecordingInfo[];
}>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
while (pagination.isTruncated) {
const nextPageUrl = `${url}?nextPageToken=${pagination.nextPageToken}`;
const nextPageResult = await get<{
pagination: any; pagination: any;
recordings: MeetRecordingInfo[]; recordings: MeetRecordingInfo[];
}>(nextPageUrl, { }>(url, {
headers: { 'x-api-key': configService.meetApiKey } headers: { 'x-api-key': configService.meetApiKey }
}); });
recordings.push(...nextPageResult.recordings);
pagination = nextPageResult.pagination;
}
return { pagination, recordings }; console.log(`Fetched initial page with ${recordings.length} recordings, isTruncated: ${pagination.isTruncated}`);
while (pagination.isTruncated) {
const nextPageUrl = `${url}?nextPageToken=${pagination.nextPageToken}`;
console.log(`Fetching next page from: ${nextPageUrl}`);
const nextPageResult = await get<{
pagination: any;
recordings: MeetRecordingInfo[];
}>(nextPageUrl, {
headers: { 'x-api-key': configService.meetApiKey }
});
recordings.push(...nextPageResult.recordings);
pagination = nextPageResult.pagination;
console.log(`Fetched additional ${nextPageResult.recordings.length} recordings, total: ${recordings.length}`);
}
console.log(`Successfully fetched total of ${recordings.length} recordings`);
return { pagination, recordings };
} catch (error) {
console.error('Error fetching all recordings:', error);
throw error;
}
}; };
export const deleteAllRecordings = async (recordingIds: string[]): Promise<void> => { export const deleteAllRecordings = async (recordingIds: string[]): Promise<void> => {
const url = `${configService.meetApiUrl}/recordings?recordingIds=${recordingIds.join(',')}`; const url = `${configService.meetApiUrl}/recordings?recordingIds=${recordingIds.join(',')}`;
await del<void>(url, { console.log(`Deleting ${recordingIds.length} recordings from: ${url}`);
headers: { 'x-api-key': configService.meetApiKey } console.log('Recording IDs to delete:', recordingIds);
}); try {
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
console.log(`Successfully deleted ${recordingIds.length} recordings`);
} catch (error) {
console.error(`Error deleting ${recordingIds.length} recordings:`, error);
console.error('Recording IDs that failed:', recordingIds);
throw error;
}
}; };

View File

@ -8,9 +8,17 @@ export async function getAllRooms(): Promise<{
rooms: MeetRoom[]; rooms: MeetRoom[];
}> { }> {
const url = `${configService.meetApiUrl}/rooms`; const url = `${configService.meetApiUrl}/rooms`;
return get<{ pagination: any; rooms: MeetRoom[] }>(url, { console.log(`Fetching all rooms from: ${url}`);
headers: { 'x-api-key': configService.meetApiKey } try {
}); const result = await get<{ pagination: any; rooms: MeetRoom[] }>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
console.log(`Successfully fetched ${result.rooms.length} rooms`);
return result;
} catch (error) {
console.error('Error fetching all rooms:', error);
throw error;
}
} }
export async function createRoom(roomData: MeetRoomOptions): Promise<MeetRoom> { export async function createRoom(roomData: MeetRoomOptions): Promise<MeetRoom> {
@ -23,23 +31,48 @@ export async function createRoom(roomData: MeetRoomOptions): Promise<MeetRoom> {
roomData.autoDeletionDate = new Date(roomData.autoDeletionDate).getTime(); roomData.autoDeletionDate = new Date(roomData.autoDeletionDate).getTime();
} }
console.log('Creating room with options:', roomData); console.log('Creating room with options:', JSON.stringify(roomData, null, 2));
return post<MeetRoom>(url, { console.log(`Making POST request to: ${url}`);
headers: { 'x-api-key': configService.meetApiKey }, try {
body: roomData const result = await post<MeetRoom>(url, {
}); headers: { 'x-api-key': configService.meetApiKey },
body: roomData
});
console.log('Room created successfully:', JSON.stringify(result, null, 2));
return result;
} catch (error) {
console.error('Error creating room:', error);
console.error('Room data that failed:', JSON.stringify(roomData, null, 2));
throw error;
}
} }
export async function deleteRoom(roomId: string): Promise<void> { export async function deleteRoom(roomId: string): Promise<void> {
const url = `${configService.meetApiUrl}/rooms/${roomId}`; const url = `${configService.meetApiUrl}/rooms/${roomId}`;
await del<void>(url, { console.log(`Deleting room ${roomId} from: ${url}`);
headers: { 'x-api-key': configService.meetApiKey } try {
}); await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
console.log(`Room ${roomId} deleted successfully`);
} catch (error) {
console.error(`Error deleting room ${roomId}:`, error);
throw error;
}
} }
export async function deleteAllRooms(roomIds: string[]): Promise<void> { export async function deleteAllRooms(roomIds: string[]): Promise<void> {
const url = `${configService.meetApiUrl}/rooms?roomIds=${roomIds.join(',')}`; const url = `${configService.meetApiUrl}/rooms?roomIds=${roomIds.join(',')}`;
await del<void>(url, { console.log(`Deleting ${roomIds.length} rooms from: ${url}`);
headers: { 'x-api-key': configService.meetApiKey } console.log('Room IDs to delete:', roomIds);
}); try {
await del<void>(url, {
headers: { 'x-api-key': configService.meetApiKey }
});
console.log(`Successfully deleted ${roomIds.length} rooms`);
} catch (error) {
console.error(`Error deleting ${roomIds.length} rooms:`, error);
console.error('Room IDs that failed:', roomIds);
throw error;
}
} }

View File

@ -18,19 +18,38 @@ async function request<T>(method: string, url: string, options: RequestOptions =
body: options.body ? JSON.stringify(options.body) : undefined body: options.body ? JSON.stringify(options.body) : undefined
}; };
const response = await fetch(fullUrl, fetchOptions); console.log(`Making ${method} request to: ${fullUrl}`);
if (!response.ok) { console.log('Request headers:', fetchOptions.headers);
try {
const response = await fetch(fullUrl, fetchOptions);
console.log(`Response status: ${response.status} ${response.statusText}`);
if (!response.ok) {
const text = await response.text();
console.error(`HTTP Error ${response.status}:`, text);
throw new Error(`HTTP ${response.status} (${response.statusText}): ${text || 'No response body'}`);
}
// Handle empty responses (e.g., for DELETE requests)
const text = await response.text(); const text = await response.text();
throw new Error(`HTTP ${response.status}: ${text}`); if (!text) {
} console.log('Empty response received');
return {} as T;
}
// Handle empty responses (e.g., for DELETE requests) console.log('Response received successfully');
const text = await response.text(); return JSON.parse(text) as T;
if (!text) { } catch (error) {
return {} as T; console.error(`Request failed for ${method} ${fullUrl}:`, error);
}
return JSON.parse(text) as T; if (error instanceof TypeError && error.message.includes('fetch')) {
throw new Error(`Network error: Unable to connect to ${fullUrl}. Check if the service is running.`);
}
throw error;
}
} }
export function get<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> { export function get<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {