Public Source Viewer
비나래아카이브 개발자 포털
실제 서비스 구조를 살펴볼 수 있는 공개용 코드 뷰어입니다. 인증, 세션, 외부 연동, 토큰, 관리자 식별 등 보안상 민감한 구현은 파일 단위 또는 줄 단위로 검열됩니다.
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