Skip to content

Commit

Permalink
feat: admin 페이지 항목 자동 추가 , 시스템 상태 그래프 추가 (#223)
Browse files Browse the repository at this point in the history
* feat: serverStatus 확인 기능 추가

* feat: serverStatus 확인 기능 추가

* feat: 모든 구독을 받아오는 기능 추가

* chore: 불필요한 상수 제거

* feat: admin page 서버 상태 확인 기능 추가
  • Loading branch information
yangdongsuk authored Dec 10, 2023
1 parent 6f0b7a8 commit 256d38f
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 16 deletions.
5 changes: 5 additions & 0 deletions server/redis/redis.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ export class RedisService {
publishToChannel(channel: string, message: string) {
this.redisPublisher.publish(channel, message);
}
psubscribeToPattern(pattern: string, callback: Function) {
this.redisSubscriber.pSubscribe(pattern, (message, channel) => {
callback(message, channel);
});
}
}
8 changes: 3 additions & 5 deletions server/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
import { Response } from 'express';
import { RedisService } from 'redis/redis.service';
import { UserId } from 'src/users/decorator/userId.decorator';
import { channels } from './const/channels.const';

@Controller('admin')
export class AdminController {
Expand All @@ -31,10 +30,9 @@ export class AdminController {
};

res.write(`data: ${changeFormat('notice', 'Server connected')}\n\n`);
channels.forEach((channel) => {
this.redisService.subscribeToChannel(channel, (message) => {
res.write(`data: ${changeFormat(channel, message)}\n\n`);
});

this.redisService.psubscribeToPattern('*', (message, channel) => {
res.write(`data: ${changeFormat(channel, message)}\n\n`);
});

req.on('close', () => {
Expand Down
7 changes: 0 additions & 7 deletions server/src/admin/const/channels.const.ts

This file was deleted.

172 changes: 168 additions & 4 deletions server/src/admin/html/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@
[data-channel='wsLog'] {
color: #fdd7ac; /* 연갈색 */
}
[data-channel='system-stats'] {
color: #f4b6c2; /* 연분홍색 */
}
[data-channel='ai_evaluate'] {
color: #ffdfba; /* 연살구색 */
}
[data-channel='ai_evaluate_error'] {
color: #ff0000; /* 빨간색 */
}

/* 로그인 폼 스타일 */
#loginForm {
Expand All @@ -102,7 +111,33 @@
color: red; /* 오류 메시지 색상 */
margin-bottom: 10px; /* 간격 */
}

/* 차트 컨테이너 스타일 */
.chart-container {
display: flex; /* Flexbox 레이아웃 사용 */
flex-wrap: wrap; /* 줄 바꿈 허용 */
justify-content: space-around; /* 요소들 사이에 공간을 균등하게 배분 */
align-items: center; /* 세로 방향 중앙 정렬 */
margin-bottom: 20px; /* 아래쪽 여백 */
}

.chart {
flex-basis: 30%; /* 각 차트가 차지할 너비 */
margin-bottom: 20px; /* 아래쪽 여백 */
}

/* 화면 너비가 768픽셀 미만일 때의 스타일 */
@media (max-width: 768px) {
.chart-container {
flex-direction: column; /* 요소들을 세로로 정렬 */
}

.chart {
flex-basis: 100%; /* 각 차트가 전체 너비를 차지하도록 설정 */
}
}
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<h1>Received Messages</h1>
Expand All @@ -113,6 +148,15 @@ <h1>Received Messages</h1>
<button class="channel-button" data-channel="ai_result">AI Result</button>
<button class="channel-button" data-channel="httpLog">httpLog</button>
<button class="channel-button" data-channel="wsLog">wsLog</button>
<button class="channel-button" data-channel="system-stats">
system-stats
</button>
<button class="channel-button" data-channel="ai_evaluate">
ai_evaluate
</button>
<button class="channel-button" data-channel="ai_evaluate_error">
ai_evaluate_error
</button>

<button id="clearButton">Clear Screen</button>
<button id="generateButton">Generate</button>
Expand All @@ -123,8 +167,32 @@ <h1>Received Messages</h1>
<input type="password" id="password" placeholder="Password" />
<button onclick="login()">Login</button>
</div>
<!-- 차트 컨테이너 -->
<div class="chart-container">
<!-- CPU 사용률 차트 -->
<div class="chart">
<h2>CPU Usage</h2>
<canvas id="cpuChart"></canvas>
</div>

<!-- 메모리 사용량 차트 -->
<div class="chart">
<h2>Memory Usage</h2>
<canvas id="memoryChart"></canvas>
</div>

<!-- 네트워크 트래픽 차트 -->
<div class="chart">
<h2>Network Traffic</h2>
<canvas id="networkChart"></canvas>
</div>
</div>
<ul id="messages"></ul>
<script>
const isLocalhost = true;
const SERVER_URL = isLocalhost
? 'http://localhost:3000/'
: 'https://openlist.kro.kr/';
const selectedChannels = new Set();
selectedChannels.add('notice');

Expand Down Expand Up @@ -157,7 +225,7 @@ <h1>Received Messages</h1>
var password = document.getElementById('password').value;
var loginError = document.getElementById('loginError');

fetch('http://localhost:3000/auth/login/admin', {
fetch(SERVER_URL + 'auth/login/admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -182,9 +250,13 @@ <h1>Received Messages</h1>
document.getElementById('loginForm').style.display = 'block';
});
}

// 그래프 관련 변수 초기화
let cpuChart, memoryChart, networkChart;
const cpuUsageData = [];
const memoryUsageData = [];
const networkTrafficData = [];
// 이벤트 소스 연결 시도
const evtSource = new EventSource(`http://localhost:3000/admin/events`, {
const evtSource = new EventSource(SERVER_URL + `admin/events`, {
withCredentials: true,
});

Expand Down Expand Up @@ -225,11 +297,103 @@ <h1>Received Messages</h1>
} else {
contentDiv.textContent = message;
}
if (channel === 'system-stats') {
const data = JSON.parse(message);
updateChartData(data);
updateCharts();
}

// 메시지 목록에 추가
messagesList.appendChild(newElement);
filterMessages(); // 새 메시지 추가 시 필터링 적용
};

// 차트 데이터 업데이트 함수
function updateChartData(data) {
const { cpuUsage, freeMemory, totalMemory, networkTraffic } = data;
const usedMemory = totalMemory - freeMemory;
const usedMemoryPercent = (usedMemory / totalMemory) * 100;
const totalNetworkTraffic =
networkTraffic.totalIpkts + networkTraffic.totalOpkts;

cpuUsageData.push(cpuUsage);
memoryUsageData.push(usedMemoryPercent);
networkTrafficData.push(totalNetworkTraffic);

// 데이터가 너무 많아지면 가장 오래된 데이터 제거
if (cpuUsageData.length > 10) cpuUsageData.shift();
if (memoryUsageData.length > 10) memoryUsageData.shift();
if (networkTrafficData.length > 10) networkTrafficData.shift();
}
function createLineChart(canvasId, data, label, borderColor) {
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: Array(data.length).fill(''), // X축 레이블
datasets: [
{
label: label,
data: data,
borderColor: borderColor,
fill: false, // 선 차트로 표시
},
],
},
options: {
scales: {
y: {
beginAtZero: true, // Y축 0부터 시작
},
},
},
});
}

function updateCharts() {
// CPU 차트 업데이트
if (!cpuChart) {
cpuChart = createLineChart(
'cpuChart',
cpuUsageData,
'CPU Usage (%)',
'rgb(255, 99, 132)',
);
} else {
cpuChart.data.datasets[0].data = cpuUsageData;
cpuChart.data.labels = Array(cpuUsageData.length).fill('');
cpuChart.update();
}

// 메모리 차트 업데이트
if (!memoryChart) {
memoryChart = createLineChart(
'memoryChart',
memoryUsageData,
'Memory Usage',
'rgb(54, 162, 235)',
);
} else {
memoryChart.data.datasets[0].data = memoryUsageData;
memoryChart.data.labels = Array(memoryUsageData.length).fill('');
memoryChart.update();
}

// 네트워크 트래픽 차트 업데이트
if (!networkChart) {
networkChart = createLineChart(
'networkChart',
networkTrafficData,
'Network Traffic',
'rgb(75, 192, 192)',
);
} else {
networkChart.data.datasets[0].data = networkTrafficData;
networkChart.data.labels = Array(networkTrafficData.length).fill('');
networkChart.update();
}
}

function filterMessages() {
const messages = document.querySelectorAll('#messages li');
messages.forEach((message) => {
Expand Down Expand Up @@ -263,7 +427,7 @@ <h1>Received Messages</h1>
.addEventListener('click', function (event) {
event.preventDefault(); // Prevents default action (navigation/refresh)

fetch('http://localhost:3000/admin/generate', {
fetch(SERVER_URL + 'admin/generate', {
credentials: 'include', // 쿠키를 포함하여 요청
})
.then((response) => {
Expand Down
95 changes: 95 additions & 0 deletions server/src/admin/js/serverStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const os = require('os');
const redis = require('redis');
const { exec } = require('child_process');
dotenv = require('dotenv');

// Redis 클라이언트 설정
const client = redis.createClient({
url: process.env.REDIS_URL,
});

client.on('error', (err) => {
console.log('Redis Client Error', err);
});

client.connect();

let lastCpuInfo = getCpuInfo();

// CPU 정보 수집 함수
function getCpuInfo() {
const cpus = os.cpus();
let totalIdle = 0,
totalTick = 0;

for (let cpu of cpus) {
for (const type in cpu.times) {
totalTick += cpu.times[type];
}
totalIdle += cpu.times.idle;
}

return { totalIdle, totalTick };
}

// CPU 사용률 계산 함수
function getCpuUsage() {
const currentCpuInfo = getCpuInfo();
const idleDifference = currentCpuInfo.totalIdle - lastCpuInfo.totalIdle;
const totalDifference = currentCpuInfo.totalTick - lastCpuInfo.totalTick;
const usage = (1 - idleDifference / totalDifference) * 100;
lastCpuInfo = currentCpuInfo;

return usage.toFixed(2);
}

// 네트워크 트래픽 정보 파싱 및 총 사용량 계산 함수
function parseNetworkTraffic(data) {
const lines = data.split('\n');
let totalIpkts = 0,
totalOpkts = 0;

for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length > 4 && parts[0] !== 'Name') {
const ipkts = parseInt(parts[4], 10);
const opkts = parseInt(parts[6], 10);
totalIpkts += ipkts;
totalOpkts += opkts;
}
}

return { totalIpkts, totalOpkts };
}

// 네트워크 트래픽 정보 수집 함수
function getNetworkTraffic(callback) {
exec('netstat -i', (err, stdout, stderr) => {
if (err) {
console.error('Error executing netstat:', err);
return;
}
callback(parseNetworkTraffic(stdout));
});
}

// 시스템 상태를 수집하고 Redis에 publish하는 함수
function publishSystemStats() {
getNetworkTraffic((networkTraffic) => {
const stats = {
freeMemory: os.freemem(),
totalMemory: os.totalmem(),
cpuUsage: getCpuUsage(),
uptime: os.uptime(),
networkTraffic: networkTraffic, // 총 네트워크 사용량 표시
};

// Redis에 publish
client.publish('system-stats', JSON.stringify(stats));

console.log('Published system stats:', stats);
});
}

// 5초마다 시스템 상태 publish
setInterval(publishSystemStats, 2000);

0 comments on commit 256d38f

Please sign in to comment.