Public Source Viewer

비나래아카이브 개발자 포털

실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.

Redacted View
src/services/monitoring.service.ts
공개 가능
1 import fs from 'fs';
2 import os from 'os';
3 import path from 'path';
4 import {
5 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
6 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
7 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
8 discordClientId,
9 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
10 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
11 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
12 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
13 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
14 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
15 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
16 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
17 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
18 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
19 vapidPublicKey
20 } from '../config/constants';
21 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
22 import { readSettings } from './settings.service';
23
24 type FileStat = {
25 name: string;
26 path: string;
27 exists: boolean;
28 sizeKb: number;
29 updatedAt: string | null;
30 records: number | null;
31 };
32
33 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
34
35 type CpuSnapshot = {
36 idle: number;
37 total: number;
38 };
39
40 type ServiceCpuSnapshot = {
41 usage: NodeJS.CpuUsage;
42 timeNs: bigint;
43 };
44
45 type NetworkSnapshot = {
46 rxBytes: number;
47 txBytes: number;
48 sampledAt: number;
49 };
50
51 let lastSystemCpu: CpuSnapshot | null = null;
52 let lastServiceCpu: ServiceCpuSnapshot | null = null;
53 let lastNetwork: NetworkSnapshot | null = null;
54
55 function countJsonRecords(filePath: string): number | null {
56 try {
57 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
58 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
59 if (!raw.trim()) return 0;
60 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
61 if (Array.isArray(parsed)) return parsed.length;
62 if (parsed && typeof parsed === 'object') return Object.keys(parsed).length;
63 return null;
64 } catch {
65 return null;
66 }
67 }
68
69 function getFileStat(name: string, filePath: string): FileStat {
70 const displayPath = path.relative(dataRoot, filePath) || path.basename(filePath);
71
72 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
73 return { name, path: displayPath, exists: false, sizeKb: 0, updatedAt: null, records: null };
74 }
75
76 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
77 return {
78 name,
79 path: displayPath,
80 exists: true,
81 sizeKb: Math.round((stat.size / 1024) * 10) / 10,
82 updatedAt: stat.mtime.toISOString(),
83 records: countJsonRecords(filePath)
84 };
85 }
86
87 function isConfigured(value: string | undefined): boolean {
88 return Boolean(value && value.trim());
89 }
90
91 function round1(value: number): number {
92 return Math.round(value * 10) / 10;
93 }
94
95 function bytesToMb(value: number): number {
96 return round1(value / 1024 / 1024);
97 }
98
99 function getSystemCpuPercent(): number | null {
100 const current = os.cpus().reduce<CpuSnapshot>((acc, cpu) => {
101 const total = Object.values(cpu.times).reduce((sum, value) => sum + value, 0);
102 return {
103 idle: acc.idle + cpu.times.idle,
104 total: acc.total + total
105 };
106 }, { idle: 0, total: 0 });
107
108 if (!lastSystemCpu) {
109 lastSystemCpu = current;
110 return null;
111 }
112
113 const idleDelta = current.idle - lastSystemCpu.idle;
114 const totalDelta = current.total - lastSystemCpu.total;
115 lastSystemCpu = current;
116
117 if (totalDelta <= 0) return null;
118 return round1(((totalDelta - idleDelta) / totalDelta) * 100);
119 }
120
121 function getServiceCpuPercent(): number | null {
122 const current: ServiceCpuSnapshot = {
123 usage: process.cpuUsage(),
124 timeNs: process.hrtime.bigint()
125 };
126
127 if (!lastServiceCpu) {
128 lastServiceCpu = current;
129 return null;
130 }
131
132 const usageDelta = process.cpuUsage(lastServiceCpu.usage);
133 const wallTimeUs = Number(current.timeNs - lastServiceCpu.timeNs) / 1000;
134 lastServiceCpu = current;
135
136 if (wallTimeUs <= 0) return null;
137 const cpuTimeUs = usageDelta.user + usageDelta.system;
138 return round1((cpuTimeUs / (wallTimeUs * Math.max(os.cpus().length, 1))) * 100);
139 }
140
141 function getDiskUsage() {
142 try {
143 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
144 const totalBytes = statfs.blocks * statfs.bsize;
145 const freeBytes = statfs.bavail * statfs.bsize;
146 const usedBytes = Math.max(totalBytes - freeBytes, 0);
147
148 return {
149 supported: true,
150 path: 'data',
151 totalMb: bytesToMb(totalBytes),
152 usedMb: bytesToMb(usedBytes),
153 freeMb: bytesToMb(freeBytes),
154 usedPercent: totalBytes > 0 ? round1((usedBytes / totalBytes) * 100) : 0
155 };
156 } catch {
157 return {
158 supported: false,
159 path: 'data',
160 totalMb: null,
161 usedMb: null,
162 freeMb: null,
163 usedPercent: null
164 };
165 }
166 }
167
168 function readLinuxNetworkSnapshot(): NetworkSnapshot | null {
169 try {
170 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
171 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
172 let rxBytes = 0;
173 let txBytes = 0;
174
175 raw.split('\n').slice(2).forEach((line) => {
176 const [ifaceRaw, statsRaw] = line.split(':');
177 if (!ifaceRaw || !statsRaw) return;
178 const iface = ifaceRaw.trim();
179 if (!iface || iface === 'lo') return;
180 const stats = statsRaw.trim().split(/\s+/).map(Number);
181 if (stats.length < 16) return;
182 rxBytes += stats[0] || 0;
183 txBytes += stats[8] || 0;
184 });
185
186 return { rxBytes, txBytes, sampledAt: Date.now() };
187 } catch {
188 return null;
189 }
190 }
191
192 function getNetworkUsage() {
193 const current = readLinuxNetworkSnapshot();
194 if (!current) {
195 return {
196 supported: false,
197 rxTotalMb: null,
198 txTotalMb: null,
199 rxRateKbps: null,
200 txRateKbps: null
201 };
202 }
203
204 const previous = lastNetwork;
205 lastNetwork = current;
206
207 if (!previous) {
208 return {
209 supported: true,
210 rxTotalMb: bytesToMb(current.rxBytes),
211 txTotalMb: bytesToMb(current.txBytes),
212 rxRateKbps: null,
213 txRateKbps: null
214 };
215 }
216
217 const elapsedSeconds = Math.max((current.sampledAt - previous.sampledAt) / 1000, 0);
218 const rxDelta = Math.max(current.rxBytes - previous.rxBytes, 0);
219 const txDelta = Math.max(current.txBytes - previous.txBytes, 0);
220
221 return {
222 supported: true,
223 rxTotalMb: bytesToMb(current.rxBytes),
224 txTotalMb: bytesToMb(current.txBytes),
225 rxRateKbps: elapsedSeconds > 0 ? round1((rxDelta / 1024) / elapsedSeconds) : null,
226 txRateKbps: elapsedSeconds > 0 ? round1((txDelta / 1024) / elapsedSeconds) : null
227 };
228 }
229
230 export function getMonitoringSnapshot() {
231 const settings = readSettings();
232 const memory = process.memoryUsage();
233 const totalMemoryMb = Math.round((os.totalmem() / 1024 / 1024) * 10) / 10;
234 const freeMemoryMb = Math.round((os.freemem() / 1024 / 1024) * 10) / 10;
235 const usedMemoryMb = Math.round((totalMemoryMb - freeMemoryMb) * 10) / 10;
236 const systemCpuPercent = getSystemCpuPercent();
237 const serviceCpuPercent = getServiceCpuPercent();
238 const disk = getDiskUsage();
239 const network = getNetworkUsage();
240
241 const dataFiles = [
242 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
243 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
244 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
245 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
246 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
247 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
248 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
249 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
250 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
251 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
252 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
253 ];
254
255 const totalDataSizeKb = dataFiles.reduce((sum, file) => sum + file.sizeKb, 0);
256
257 return {
258 generatedAt: new Date().toISOString(),
259 process: {
260 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
261 nodeVersion: process.version,
262 uptimeSeconds: Math.floor(process.uptime()),
263 memoryRssMb: Math.round((memory.rss / 1024 / 1024) * 10) / 10,
264 memoryHeapUsedMb: Math.round((memory.heapUsed / 1024 / 1024) * 10) / 10,
265 systemMemoryTotalMb: totalMemoryMb,
266 systemMemoryUsedMb: usedMemoryMb,
267 systemMemoryFreeMb: freeMemoryMb,
268 systemMemoryUsedPercent: totalMemoryMb > 0 ? Math.round((usedMemoryMb / totalMemoryMb) * 1000) / 10 : 0,
269 systemCpuPercent,
270 serviceCpuPercent,
271 cpuCores: os.cpus().length
272 },
273 disk,
274 network,
275 integrations: [
276 {
277 key: 'openai',
278 name: 'OpenAI API',
279 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
280 enabled: settings.isGptEnabled !== false,
281 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
282 accessLabel: settings.isGptEnabled !== false ? 'Everyone' : 'Admin Only',
283 accessTone: settings.isGptEnabled !== false ? 'on' : 'warn'
284 },
285 {
286 key: 'google',
287 name: 'Google Gemini API',
288 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
289 enabled: true,
290 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
291 },
292 {
293 key: 'subway',
294 name: 'Seoul Subway API',
295 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
296 enabled: settings.isSubwayApiEnabled !== false,
297 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
298 },
299 {
300 key: 'push',
301 name: 'Web Push',
302 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
303 enabled: settings.isPushEnabled !== false,
304 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
305 },
306 {
307 key: 'discord-bot',
308 name: 'Discord Bot',
309 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
310 enabled: (settings.isDiscordAiCommandEnabled !== false) || (settings.isDiscordPersonaCommandEnabled !== false),
311 [SECURITY REDACTED] 민감한 설정/인증/토큰 관련 코드입니다.
312 }
313 ],
314 featureFlags: [
315 { key: 'isSignupEnabled', name: '회원가입', enabled: settings.isSignupEnabled !== false, description: '새 계정 생성 허용' },
316 { key: 'isAnonymousPostingEnabled', name: '익명 글쓰기', enabled: settings.isAnonymousPostingEnabled !== false, description: 'Archive/Blog 익명 작성 허용' },
317 {
318 key: 'isGptEnabled',
319 name: 'GPT/AI',
320 enabled: settings.isGptEnabled !== false,
321 description: settings.isGptEnabled !== false
322 ? '모든 사용자가 웹 AI 기능을 사용할 수 있습니다.'
323 : '비나래 관리자만 웹 AI 기능을 사용할 수 있습니다.',
324 stateLabel: settings.isGptEnabled !== false ? '모두 사용 가능' : '관리자만 사용 가능',
325 stateTone: settings.isGptEnabled !== false ? 'on' : 'warn'
326 },
327 { key: 'isSubwayApiEnabled', name: '지하철 API', enabled: settings.isSubwayApiEnabled !== false, description: '실시간 열차/도착 API 사용 허용' },
328 { key: 'isPushEnabled', name: '푸시 알림', enabled: settings.isPushEnabled !== false, description: 'Web Push 및 FCM 토큰 등록 허용' },
329 { key: 'isDiscordShareEnabled', name: 'Discord 공유', enabled: settings.isDiscordShareEnabled !== false, description: 'AI 답변 웹훅 공유 허용' },
330 { key: 'isDiscordPersonaCommandEnabled', name: 'Discord 페르소나 명령', enabled: settings.isDiscordPersonaCommandEnabled !== false, description: '페르소나 설정 링크 명령 허용' },
331 { key: 'isDiscordAiCommandEnabled', name: 'Discord AI 명령', enabled: settings.isDiscordAiCommandEnabled !== false, description: 'Discord AI 응답 명령 허용' }
332 ],
333 dataFiles,
334 dataSummary: {
335 fileCount: dataFiles.length,
336 existingFileCount: dataFiles.filter((file) => file.exists).length,
337 totalDataSizeKb: Math.round(totalDataSizeKb * 10) / 10
338 }
339 };
340 }
341