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