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
[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