directus

Форк
0
/
server.ts 
447 строк · 12.5 Кб
1
import { useEnv } from '@directus/env';
2
import type { Accountability, SchemaOverview } from '@directus/types';
3
import { toArray, toBoolean } from '@directus/utils';
4
import { version } from 'directus/version';
5
import type { Knex } from 'knex';
6
import { merge } from 'lodash-es';
7
import { Readable } from 'node:stream';
8
import { performance } from 'perf_hooks';
9
import { getCache } from '../cache.js';
10
import getDatabase, { hasDatabaseConnection } from '../database/index.js';
11
import { useLogger } from '../logger.js';
12
import getMailer from '../mailer.js';
13
import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
14
import { rateLimiter } from '../middleware/rate-limiter-ip.js';
15
import { SERVER_ONLINE } from '../server.js';
16
import { getStorage } from '../storage/index.js';
17
import type { AbstractServiceOptions } from '../types/index.js';
18
import { SettingsService } from './settings.js';
19

20
const env = useEnv();
21
const logger = useLogger();
22

23
export class ServerService {
24
	knex: Knex;
25
	accountability: Accountability | null;
26
	settingsService: SettingsService;
27
	schema: SchemaOverview;
28

29
	constructor(options: AbstractServiceOptions) {
30
		this.knex = options.knex || getDatabase();
31
		this.accountability = options.accountability || null;
32
		this.schema = options.schema;
33
		this.settingsService = new SettingsService({ knex: this.knex, schema: this.schema });
34
	}
35

36
	async serverInfo(): Promise<Record<string, any>> {
37
		const info: Record<string, any> = {};
38

39
		const projectInfo = await this.settingsService.readSingleton({
40
			fields: [
41
				'project_name',
42
				'project_descriptor',
43
				'project_logo',
44
				'project_color',
45
				'default_appearance',
46
				'default_theme_light',
47
				'default_theme_dark',
48
				'theme_light_overrides',
49
				'theme_dark_overrides',
50
				'default_language',
51
				'public_foreground',
52
				'public_background.id',
53
				'public_background.type',
54
				'public_favicon',
55
				'public_note',
56
				'custom_css',
57
				'public_registration',
58
				'public_registration_verify_email',
59
			],
60
		});
61

62
		info['project'] = projectInfo;
63

64
		if (this.accountability?.user) {
65
			if (env['RATE_LIMITER_ENABLED']) {
66
				info['rateLimit'] = {
67
					points: env['RATE_LIMITER_POINTS'],
68
					duration: env['RATE_LIMITER_DURATION'],
69
				};
70
			} else {
71
				info['rateLimit'] = false;
72
			}
73

74
			if (env['RATE_LIMITER_GLOBAL_ENABLED']) {
75
				info['rateLimitGlobal'] = {
76
					points: env['RATE_LIMITER_GLOBAL_POINTS'],
77
					duration: env['RATE_LIMITER_GLOBAL_DURATION'],
78
				};
79
			} else {
80
				info['rateLimitGlobal'] = false;
81
			}
82

83
			info['extensions'] = {
84
				limit: env['EXTENSIONS_LIMIT'] ?? null,
85
			};
86

87
			info['queryLimit'] = {
88
				default: env['QUERY_LIMIT_DEFAULT'],
89
				max: Number.isFinite(env['QUERY_LIMIT_MAX']) ? env['QUERY_LIMIT_MAX'] : -1,
90
			};
91

92
			if (toBoolean(env['WEBSOCKETS_ENABLED'])) {
93
				info['websocket'] = {};
94

95
				info['websocket'].rest = toBoolean(env['WEBSOCKETS_REST_ENABLED'])
96
					? {
97
							authentication: env['WEBSOCKETS_REST_AUTH'],
98
							path: env['WEBSOCKETS_REST_PATH'],
99
					  }
100
					: false;
101

102
				info['websocket'].graphql = toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED'])
103
					? {
104
							authentication: env['WEBSOCKETS_GRAPHQL_AUTH'],
105
							path: env['WEBSOCKETS_GRAPHQL_PATH'],
106
					  }
107
					: false;
108

109
				info['websocket'].heartbeat = toBoolean(env['WEBSOCKETS_HEARTBEAT_ENABLED'])
110
					? env['WEBSOCKETS_HEARTBEAT_PERIOD']
111
					: false;
112
			} else {
113
				info['websocket'] = false;
114
			}
115

116
			info['version'] = version;
117
		}
118

119
		return info;
120
	}
121

122
	async health(): Promise<Record<string, any>> {
123
		const { nanoid } = await import('nanoid');
124

125
		const checkID = nanoid(5);
126

127
		// Based on https://tools.ietf.org/id/draft-inadarei-api-health-check-05.html#name-componenttype
128
		type HealthData = {
129
			status: 'ok' | 'warn' | 'error';
130
			releaseId: string;
131
			serviceId: string;
132
			checks: {
133
				[service: string]: HealthCheck[];
134
			};
135
		};
136

137
		type HealthCheck = {
138
			componentType: 'system' | 'datastore' | 'objectstore' | 'email' | 'cache' | 'ratelimiter';
139
			observedValue?: number | string | boolean;
140
			observedUnit?: string;
141
			status: 'ok' | 'warn' | 'error';
142
			output?: any;
143
			threshold?: number;
144
		};
145

146
		const data: HealthData = {
147
			status: 'ok',
148
			releaseId: version,
149
			serviceId: env['PUBLIC_URL'] as string,
150
			checks: merge(
151
				...(await Promise.all([
152
					testDatabase(),
153
					testCache(),
154
					testRateLimiter(),
155
					testRateLimiterGlobal(),
156
					testStorage(),
157
					testEmail(),
158
				])),
159
			),
160
		};
161

162
		if (SERVER_ONLINE === false) {
163
			data.status = 'error';
164
		}
165

166
		for (const [service, healthData] of Object.entries(data.checks)) {
167
			for (const healthCheck of healthData) {
168
				if (healthCheck.status === 'warn' && data.status === 'ok') {
169
					logger.warn(
170
						`${service} in WARN state, the observed value ${healthCheck.observedValue} is above the threshold of ${healthCheck.threshold}${healthCheck.observedUnit}`,
171
					);
172

173
					data.status = 'warn';
174
					continue;
175
				}
176

177
				if (healthCheck.status === 'error' && (data.status === 'ok' || data.status === 'warn')) {
178
					logger.error(healthCheck.output, '%s in ERROR state', service);
179
					data.status = 'error';
180
					break;
181
				}
182
			}
183

184
			// No need to continue checking if parent status is already error
185
			if (data.status === 'error') break;
186
		}
187

188
		if (this.accountability?.admin !== true) {
189
			return { status: data.status };
190
		} else {
191
			return data;
192
		}
193

194
		async function testDatabase(): Promise<Record<string, HealthCheck[]>> {
195
			const database = getDatabase();
196
			const client = env['DB_CLIENT'];
197

198
			const checks: Record<string, HealthCheck[]> = {};
199

200
			// Response time
201
			// ----------------------------------------------------------------------------------------
202
			checks[`${client}:responseTime`] = [
203
				{
204
					status: 'ok',
205
					componentType: 'datastore',
206
					observedUnit: 'ms',
207
					observedValue: 0,
208
					threshold: env['DB_HEALTHCHECK_THRESHOLD'] ? +env['DB_HEALTHCHECK_THRESHOLD'] : 150,
209
				},
210
			];
211

212
			const startTime = performance.now();
213

214
			if (await hasDatabaseConnection()) {
215
				checks[`${client}:responseTime`]![0]!.status = 'ok';
216
			} else {
217
				checks[`${client}:responseTime`]![0]!.status = 'error';
218
				checks[`${client}:responseTime`]![0]!.output = `Can't connect to the database.`;
219
			}
220

221
			const endTime = performance.now();
222
			checks[`${client}:responseTime`]![0]!.observedValue = +(endTime - startTime).toFixed(3);
223

224
			if (
225
				Number(checks[`${client}:responseTime`]![0]!.observedValue!) >
226
					checks[`${client}:responseTime`]![0]!.threshold! &&
227
				checks[`${client}:responseTime`]![0]!.status !== 'error'
228
			) {
229
				checks[`${client}:responseTime`]![0]!.status = 'warn';
230
			}
231

232
			checks[`${client}:connectionsAvailable`] = [
233
				{
234
					status: 'ok',
235
					componentType: 'datastore',
236
					observedValue: database.client.pool.numFree(),
237
				},
238
			];
239

240
			checks[`${client}:connectionsUsed`] = [
241
				{
242
					status: 'ok',
243
					componentType: 'datastore',
244
					observedValue: database.client.pool.numUsed(),
245
				},
246
			];
247

248
			return checks;
249
		}
250

251
		async function testCache(): Promise<Record<string, HealthCheck[]>> {
252
			if (env['CACHE_ENABLED'] !== true) {
253
				return {};
254
			}
255

256
			const { cache } = getCache();
257

258
			const checks: Record<string, HealthCheck[]> = {
259
				'cache:responseTime': [
260
					{
261
						status: 'ok',
262
						componentType: 'cache',
263
						observedValue: 0,
264
						observedUnit: 'ms',
265
						threshold: env['CACHE_HEALTHCHECK_THRESHOLD'] ? +env['CACHE_HEALTHCHECK_THRESHOLD'] : 150,
266
					},
267
				],
268
			};
269

270
			const startTime = performance.now();
271

272
			try {
273
				await cache!.set(`health-${checkID}`, true, 5);
274
				await cache!.delete(`health-${checkID}`);
275
			} catch (err: any) {
276
				checks['cache:responseTime']![0]!.status = 'error';
277
				checks['cache:responseTime']![0]!.output = err;
278
			} finally {
279
				const endTime = performance.now();
280
				checks['cache:responseTime']![0]!.observedValue = +(endTime - startTime).toFixed(3);
281

282
				if (
283
					checks['cache:responseTime']![0]!.observedValue > checks['cache:responseTime']![0]!.threshold! &&
284
					checks['cache:responseTime']![0]!.status !== 'error'
285
				) {
286
					checks['cache:responseTime']![0]!.status = 'warn';
287
				}
288
			}
289

290
			return checks;
291
		}
292

293
		async function testRateLimiter(): Promise<Record<string, HealthCheck[]>> {
294
			if (env['RATE_LIMITER_ENABLED'] !== true) {
295
				return {};
296
			}
297

298
			const checks: Record<string, HealthCheck[]> = {
299
				'rateLimiter:responseTime': [
300
					{
301
						status: 'ok',
302
						componentType: 'ratelimiter',
303
						observedValue: 0,
304
						observedUnit: 'ms',
305
						threshold: env['RATE_LIMITER_HEALTHCHECK_THRESHOLD'] ? +env['RATE_LIMITER_HEALTHCHECK_THRESHOLD'] : 150,
306
					},
307
				],
308
			};
309

310
			const startTime = performance.now();
311

312
			try {
313
				await rateLimiter.consume(`health-${checkID}`, 1);
314
				await rateLimiter.delete(`health-${checkID}`);
315
			} catch (err: any) {
316
				checks['rateLimiter:responseTime']![0]!.status = 'error';
317
				checks['rateLimiter:responseTime']![0]!.output = err;
318
			} finally {
319
				const endTime = performance.now();
320
				checks['rateLimiter:responseTime']![0]!.observedValue = +(endTime - startTime).toFixed(3);
321

322
				if (
323
					checks['rateLimiter:responseTime']![0]!.observedValue > checks['rateLimiter:responseTime']![0]!.threshold! &&
324
					checks['rateLimiter:responseTime']![0]!.status !== 'error'
325
				) {
326
					checks['rateLimiter:responseTime']![0]!.status = 'warn';
327
				}
328
			}
329

330
			return checks;
331
		}
332

333
		async function testRateLimiterGlobal(): Promise<Record<string, HealthCheck[]>> {
334
			if (env['RATE_LIMITER_GLOBAL_ENABLED'] !== true) {
335
				return {};
336
			}
337

338
			const checks: Record<string, HealthCheck[]> = {
339
				'rateLimiterGlobal:responseTime': [
340
					{
341
						status: 'ok',
342
						componentType: 'ratelimiter',
343
						observedValue: 0,
344
						observedUnit: 'ms',
345
						threshold: env['RATE_LIMITER_GLOBAL_HEALTHCHECK_THRESHOLD']
346
							? +env['RATE_LIMITER_GLOBAL_HEALTHCHECK_THRESHOLD']
347
							: 150,
348
					},
349
				],
350
			};
351

352
			const startTime = performance.now();
353

354
			try {
355
				await rateLimiterGlobal.consume(`health-${checkID}`, 1);
356
				await rateLimiterGlobal.delete(`health-${checkID}`);
357
			} catch (err: any) {
358
				checks['rateLimiterGlobal:responseTime']![0]!.status = 'error';
359
				checks['rateLimiterGlobal:responseTime']![0]!.output = err;
360
			} finally {
361
				const endTime = performance.now();
362
				checks['rateLimiterGlobal:responseTime']![0]!.observedValue = +(endTime - startTime).toFixed(3);
363

364
				if (
365
					checks['rateLimiterGlobal:responseTime']![0]!.observedValue >
366
						checks['rateLimiterGlobal:responseTime']![0]!.threshold! &&
367
					checks['rateLimiterGlobal:responseTime']![0]!.status !== 'error'
368
				) {
369
					checks['rateLimiterGlobal:responseTime']![0]!.status = 'warn';
370
				}
371
			}
372

373
			return checks;
374
		}
375

376
		async function testStorage(): Promise<Record<string, HealthCheck[]>> {
377
			const storage = await getStorage();
378

379
			const checks: Record<string, HealthCheck[]> = {};
380

381
			for (const location of toArray(env['STORAGE_LOCATIONS'] as string)) {
382
				const disk = storage.location(location);
383
				const envThresholdKey = `STORAGE_${location}_HEALTHCHECK_THRESHOLD`.toUpperCase();
384

385
				checks[`storage:${location}:responseTime`] = [
386
					{
387
						status: 'ok',
388
						componentType: 'objectstore',
389
						observedValue: 0,
390
						observedUnit: 'ms',
391
						threshold: env[envThresholdKey] ? +(env[envThresholdKey] as string) : 750,
392
					},
393
				];
394

395
				const startTime = performance.now();
396

397
				try {
398
					await disk.write(`health-${checkID}`, Readable.from(['check']));
399
					const fileStream = await disk.read(`health-${checkID}`);
400

401
					fileStream.on('data', async () => {
402
						fileStream.destroy();
403
						await disk.delete(`health-${checkID}`);
404
					});
405
				} catch (err: any) {
406
					checks[`storage:${location}:responseTime`]![0]!.status = 'error';
407
					checks[`storage:${location}:responseTime`]![0]!.output = err;
408
				} finally {
409
					const endTime = performance.now();
410
					checks[`storage:${location}:responseTime`]![0]!.observedValue = +(endTime - startTime).toFixed(3);
411

412
					if (
413
						Number(checks[`storage:${location}:responseTime`]![0]!.observedValue!) >
414
							checks[`storage:${location}:responseTime`]![0]!.threshold! &&
415
						checks[`storage:${location}:responseTime`]![0]!.status !== 'error'
416
					) {
417
						checks[`storage:${location}:responseTime`]![0]!.status = 'warn';
418
					}
419
				}
420
			}
421

422
			return checks;
423
		}
424

425
		async function testEmail(): Promise<Record<string, HealthCheck[]>> {
426
			const checks: Record<string, HealthCheck[]> = {
427
				'email:connection': [
428
					{
429
						status: 'ok',
430
						componentType: 'email',
431
					},
432
				],
433
			};
434

435
			const mailer = getMailer();
436

437
			try {
438
				await mailer.verify();
439
			} catch (err: any) {
440
				checks['email:connection']![0]!.status = 'error';
441
				checks['email:connection']![0]!.output = err;
442
			}
443

444
			return checks;
445
		}
446
	}
447
}
448

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.