Skip to content

Commit 592647b

Browse files
authored
Merge pull request #528 from VipinDevelops/system-status
[FEAT]: System Logs
2 parents aa0c53d + a0d1f24 commit 592647b

File tree

12 files changed

+612
-3
lines changed

12 files changed

+612
-3
lines changed

web-server/app/api/stream/route.ts

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { exec } from 'child_process';
2+
import { createReadStream, FSWatcher, watch } from 'fs';
3+
4+
import {
5+
getServerStatusCode,
6+
getSyncServerStatusCode
7+
} from '@/api-helpers/axios';
8+
import { ServiceNames } from '@/constants/service';
9+
import {
10+
UPDATE_INTERVAL,
11+
LogFile,
12+
LOG_FILES,
13+
StreamEventType,
14+
FileEvent,
15+
SendEventData
16+
} from '@/constants/stream';
17+
18+
async function executeCommand(command: string): Promise<string> {
19+
return new Promise((resolve, reject) => {
20+
exec(command, (error, stdout) => {
21+
if (error) {
22+
reject(error);
23+
} else {
24+
resolve(stdout.trim());
25+
}
26+
});
27+
});
28+
}
29+
30+
async function isApiServerUp(): Promise<boolean> {
31+
try {
32+
const statusCode = await getServerStatusCode('');
33+
return statusCode === 200;
34+
} catch {
35+
return false;
36+
}
37+
}
38+
39+
async function isSyncServerUp(): Promise<boolean> {
40+
try {
41+
const statusCode = await getSyncServerStatusCode('');
42+
return statusCode === 200;
43+
} catch {
44+
return false;
45+
}
46+
}
47+
48+
async function isRedisUp(): Promise<boolean> {
49+
try {
50+
const response = await executeCommand(
51+
`redis-cli -p ${process.env.REDIS_PORT} ping`
52+
);
53+
return response.includes('PONG');
54+
} catch {
55+
return false;
56+
}
57+
}
58+
59+
async function isPostgresUp(): Promise<boolean> {
60+
try {
61+
const response = await executeCommand(
62+
`pg_isready -h ${process.env.DB_HOST} -p ${process.env.DB_PORT}`
63+
);
64+
return response.includes('accepting connections');
65+
} catch {
66+
return false;
67+
}
68+
}
69+
70+
async function checkServiceStatus(serviceName: ServiceNames): Promise<boolean> {
71+
const statusCheckers = {
72+
[ServiceNames.API_SERVER]: isApiServerUp,
73+
[ServiceNames.SYNC_SERVER]: isSyncServerUp,
74+
[ServiceNames.REDIS]: isRedisUp,
75+
[ServiceNames.POSTGRES]: isPostgresUp
76+
};
77+
78+
const checker = statusCheckers[serviceName];
79+
if (!checker) {
80+
console.warn(`Service ${serviceName} not recognized.`);
81+
return false;
82+
}
83+
84+
try {
85+
return await checker();
86+
} catch (error) {
87+
console.error(`${serviceName} service is down:`, error);
88+
return false;
89+
}
90+
}
91+
92+
async function getAllServicesStatus(): Promise<
93+
Record<ServiceNames, { isUp: boolean }>
94+
> {
95+
const services = Object.values(ServiceNames);
96+
const statusPromises = services.map(async (service) => [
97+
service,
98+
{ isUp: await checkServiceStatus(service) }
99+
]);
100+
101+
const statuses = Object.fromEntries(await Promise.all(statusPromises));
102+
return statuses as Record<ServiceNames, { isUp: boolean }>;
103+
}
104+
105+
// Creates an event message for the stream.
106+
function createEventMessage(
107+
eventType: StreamEventType,
108+
data: SendEventData
109+
): Uint8Array {
110+
const encoder = new TextEncoder();
111+
return encoder.encode(
112+
`data: ${JSON.stringify({ type: eventType, ...data })}\n\n`
113+
);
114+
}
115+
116+
export async function GET(): Promise<Response> {
117+
let isStreamActive = true;
118+
const filePositions: Record<string, number> = {};
119+
let statusUpdateTimer: NodeJS.Timeout | null = null;
120+
const fileWatchers: FSWatcher[] = [];
121+
122+
const stream = new ReadableStream({
123+
start(controller) {
124+
// Sends status updates periodically.
125+
async function sendStatusUpdates() {
126+
if (!isStreamActive) return;
127+
128+
try {
129+
const statuses = await getAllServicesStatus();
130+
if (isStreamActive) {
131+
controller.enqueue(
132+
createEventMessage(StreamEventType.StatusUpdate, { statuses })
133+
);
134+
}
135+
} catch (error) {
136+
console.error('Error sending statuses:', error);
137+
}
138+
139+
if (isStreamActive) {
140+
statusUpdateTimer = setTimeout(sendStatusUpdates, UPDATE_INTERVAL);
141+
}
142+
}
143+
144+
// Sends log file updates.
145+
async function sendLogUpdates(logFile: LogFile) {
146+
if (!isStreamActive) return;
147+
148+
try {
149+
const { path, serviceName } = logFile;
150+
const fileStream = createReadStream(path, {
151+
start: filePositions[path] || 0,
152+
encoding: 'utf8'
153+
});
154+
155+
for await (const chunk of fileStream) {
156+
if (!isStreamActive) break;
157+
controller.enqueue(
158+
createEventMessage(StreamEventType.LogUpdate, {
159+
serviceName,
160+
content: chunk
161+
})
162+
);
163+
filePositions[path] =
164+
(filePositions[path] || 0) + Buffer.byteLength(chunk);
165+
}
166+
} catch (error) {
167+
console.error(
168+
`Error reading log file for ${logFile.serviceName}:`,
169+
error
170+
);
171+
}
172+
}
173+
174+
// Sets up file watchers for log files.
175+
function setupFileWatchers() {
176+
LOG_FILES.forEach((logFile) => {
177+
const watcher = watch(logFile.path, async (eventType) => {
178+
if (eventType === FileEvent.Change && isStreamActive) {
179+
await sendLogUpdates(logFile);
180+
}
181+
});
182+
fileWatchers.push(watcher);
183+
});
184+
}
185+
186+
// Initialize the stream
187+
sendStatusUpdates();
188+
LOG_FILES.forEach(sendLogUpdates);
189+
setupFileWatchers();
190+
},
191+
cancel() {
192+
isStreamActive = false;
193+
if (statusUpdateTimer) clearTimeout(statusUpdateTimer);
194+
fileWatchers.forEach((watcher) => watcher.close());
195+
}
196+
});
197+
198+
return new Response(stream, {
199+
headers: {
200+
'Content-Type': 'text/event-stream; charset=utf-8',
201+
Connection: 'keep-alive',
202+
'Cache-Control': 'no-cache, no-transform',
203+
'X-Accel-Buffering': 'no',
204+
'Content-Encoding': 'none'
205+
}
206+
});
207+
}

web-server/pages/system-logs.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Authenticated } from 'src/components/Authenticated';
2+
3+
import { FlexBox } from '@/components/FlexBox';
4+
import { SystemStatus } from '@/components/Service/SystemStatus';
5+
import { useRedirectWithSession } from '@/constants/useRoute';
6+
import { PageWrapper } from '@/content/PullRequests/PageWrapper';
7+
import ExtendedSidebarLayout from '@/layouts/ExtendedSidebarLayout';
8+
import { useSelector } from '@/store';
9+
import { PageLayout } from '@/types/resources';
10+
11+
function Service() {
12+
useRedirectWithSession();
13+
14+
const loading = useSelector((state) => state.service.loading);
15+
16+
return (
17+
<PageWrapper
18+
title={
19+
<FlexBox gap={1} alignCenter>
20+
System logs
21+
</FlexBox>
22+
}
23+
hideAllSelectors
24+
pageTitle="System logs"
25+
showEvenIfNoTeamSelected={true}
26+
isLoading={loading}
27+
>
28+
<SystemStatus />
29+
</PageWrapper>
30+
);
31+
}
32+
33+
Service.getLayout = (page: PageLayout) => (
34+
<Authenticated>
35+
<ExtendedSidebarLayout>{page}</ExtendedSidebarLayout>
36+
</Authenticated>
37+
);
38+
39+
export default Service;

web-server/src/api-helpers/axios.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,35 @@ export const handleSyncServerRequest = <T = any>(
9090
})
9191
.then(handleThen)
9292
.catch(handleCatch);
93+
94+
const getStatusCode = async (
95+
url: string,
96+
params: AxiosRequestConfig<any> = { method: 'get' },
97+
server: 'internal' | 'sync' = 'internal'
98+
): Promise<number> => {
99+
const instance = server === 'internal' ? internal : internalSyncServer;
100+
101+
try {
102+
const response = await instance({
103+
url,
104+
...params,
105+
headers: { 'Content-Type': 'application/json' }
106+
});
107+
return response.status;
108+
} catch (error: any) {
109+
if (error.response) {
110+
return error.response.status;
111+
}
112+
throw error;
113+
}
114+
};
115+
116+
export const getServerStatusCode = (
117+
url: string,
118+
params?: AxiosRequestConfig<any>
119+
) => getStatusCode(url, params, 'internal');
120+
121+
export const getSyncServerStatusCode = (
122+
url: string,
123+
params?: AxiosRequestConfig<any>
124+
) => getStatusCode(url, params, 'sync');

0 commit comments

Comments
 (0)