import { Knex } from 'knex' ; import { merge } from 'lodash' ; import macosRelease from 'macos-release' ; import { nanoid } from 'nanoid' ; import os from 'os' ; import { performance } from 'perf_hooks' ; import { version } from '../../package.json' ; import { getCache } from '../cache' ; import getDatabase , { hasDatabaseConnection } from '../database' ; import env from '../env' ; import logger from '../logger' ; import { rateLimiter } from '../middleware/rate-limiter' ; import storage from '../storage' ; import { AbstractServiceOptions , SchemaOverview } from '../types' ; import { Accountability } from '@directus/shared/types' ; import { toArray } from '@directus/shared/utils' ; import getMailer from '../mailer' ; import { SettingsService } from './settings' ; export class ServerService { knex : Knex ; accountability : Accountability | null ; settingsService : SettingsService ; schema : SchemaOverview ; constructor ( options ) { this . knex = options . knex || getDatabase ( ) ; this . accountability = options . accountability || null ; this . schema = options . schema ; this . settingsService = new SettingsService ( { knex : this . knex , schema : this . schema } ) ; } async serverInfo ( ) { const info = { } ; const projectInfo = await this . settingsService . readSingleton ( { fields : [ 'project_name' , 'project_descriptor' , 'project_logo' , 'project_color' , 'public_foreground' , 'public_background' , 'public_note' , 'custom_css' , ] , } ) ; info . project = projectInfo ; if ( this . accountability ?. admin === true ) { const osType = os . type ( ) === 'Darwin' ? 'macOS' : os . type ( ) ; const osVersion = osType === 'macOS' ? `${ macosRelease ( ) . name } (${ macosRelease ( ) . version })` : os . release ( ) ; info . directus = { version , } ; info . node = { version : process . versions . node , uptime : Math . round ( process . uptime ( ) ) , } ; info . os = { type : osType , version : osVersion , uptime : Math . round ( os . uptime ( ) ) , totalmem : os . totalmem ( ) , } ; } return info ; } async health ( ) { const checkID = nanoid ( 5 ) ; type HealthData = { status : 'ok' | 'warn' | 'error' ; releaseId : string ; serviceId : string ; checks : { [ service ] : HealthCheck [ ] ; } ; } ; type HealthCheck = { componentType : 'system' | 'datastore' | 'objectstore' | 'email' | 'cache' | 'ratelimiter' ; observedValue ? : number | string | boolean ; observedUnit ? : string ; status : 'ok' | 'warn' | 'error' ; output ? : any ; threshold ? : number ; } ; const data = { status : 'ok' , releaseId : version , serviceId : env . KEY , checks : merge ( ... ( await Promise . all ( [ testDatabase ( ) , testCache ( ) , testRateLimiter ( ) , testStorage ( ) , testEmail ( ) ] ) ) ) , } ; for ( const [ service , healthData ] of Object . entries ( data . checks ) ) { for ( const healthCheck of healthData ) { if ( healthCheck . status === 'warn' && data . status === 'ok' ) { logger . warn ( `${ service } in WARN state, the observed value ${ healthCheck . observedValue } is above the threshold of ${ healthCheck . threshold }${ healthCheck . observedUnit }` ) ; data . status = 'warn' ; continue ; } if ( healthCheck . status === 'error' && ( data . status === 'ok' || data . status === 'warn' ) ) { logger . error ( healthCheck . output , '%s in ERROR state' , service ) ; data . status = 'error' ; break ; } } if ( data . status === 'error' ) break ; } if ( this . accountability ?. admin !== true ) { return { status : data . status } ; } else { return data ; } async function testDatabase ( ) { const database = getDatabase ( ) ; const client = env . DB_CLIENT ; const checks = { } ; checks [ `${ client }:responseTime` ] = [ { status : 'ok' , componentType : 'datastore' , observedUnit : 'ms' , observedValue : 0 , threshold : 150 , } , ] ; const startTime = performance . now ( ) ; if ( await hasDatabaseConnection ( ) ) { checks [ `${ client }:responseTime` ] [ 0 ] . status = 'ok' ; } else { checks [ `${ client }:responseTime` ] [ 0 ] . status = 'error' ; checks [ `${ client }:responseTime` ] [ 0 ] . output = `Can't connect to the database.` ; } const endTime = performance . now ( ) ; checks [ `${ client }:responseTime` ] [ 0 ] . observedValue = + ( endTime - startTime ) . toFixed ( 3 ) ; if ( checks [ `${ client }:responseTime` ] [ 0 ] . observedValue ! > checks [ `${ client }:responseTime` ] [ 0 ] . threshold ! && checks [ `${ client }:responseTime` ] [ 0 ] . status !== 'error' ) { checks [ `${ client }:responseTime` ] [ 0 ] . status = 'warn' ; } checks [ `${ client }:connectionsAvailable` ] = [ { status : 'ok' , componentType : 'datastore' , observedValue : database . client . pool . numFree ( ) , } , ] ; checks [ `${ client }:connectionsUsed` ] = [ { status : 'ok' , componentType : 'datastore' , observedValue : database . client . pool . numUsed ( ) , } , ] ; return checks ; } async function testCache ( ) { if ( env . CACHE_ENABLED !== true ) { return { } ; } const { cache } = getCache ( ) ; const checks = { 'cache:responseTime' : [ { status : 'ok' , componentType : 'cache' , observedValue : 0 , observedUnit : 'ms' , threshold : 150 , } , ] , } ; const startTime = performance . now ( ) ; try { await cache ! . set ( `health-${ checkID }` , true , 5 ) ; await cache ! . delete ( `health-${ checkID }` ) ; } catch ( err ) { checks [ 'cache:responseTime' ] [ 0 ] . status = 'error' ; checks [ 'cache:responseTime' ] [ 0 ] . output = err ; } finally { const endTime = performance . now ( ) ; checks [ 'cache:responseTime' ] [ 0 ] . observedValue = + ( endTime - startTime ) . toFixed ( 3 ) ; if ( checks [ 'cache:responseTime' ] [ 0 ] . observedValue > checks [ 'cache:responseTime' ] [ 0 ] . threshold ! && checks [ 'cache:responseTime' ] [ 0 ] . status !== 'error' ) { checks [ 'cache:responseTime' ] [ 0 ] . status = 'warn' ; } } return checks ; } async function testRateLimiter ( ) { if ( env . RATE_LIMITER_ENABLED !== true ) { return { } ; } const checks = { 'rateLimiter:responseTime' : [ { status : 'ok' , componentType : 'ratelimiter' , observedValue : 0 , observedUnit : 'ms' , threshold : 150 , } , ] , } ; const startTime = performance . now ( ) ; try { await rateLimiter . consume ( `health-${ checkID }` , 1 ) ; await rateLimiter . delete ( `health-${ checkID }` ) ; } catch ( err ) { checks [ 'rateLimiter:responseTime' ] [ 0 ] . status = 'error' ; checks [ 'rateLimiter:responseTime' ] [ 0 ] . output = err ; } finally { const endTime = performance . now ( ) ; checks [ 'rateLimiter:responseTime' ] [ 0 ] . observedValue = + ( endTime - startTime ) . toFixed ( 3 ) ; if ( checks [ 'rateLimiter:responseTime' ] [ 0 ] . observedValue > checks [ 'rateLimiter:responseTime' ] [ 0 ] . threshold ! && checks [ 'rateLimiter:responseTime' ] [ 0 ] . status !== 'error' ) { checks [ 'rateLimiter:responseTime' ] [ 0 ] . status = 'warn' ; } } return checks ; } async function testStorage ( ) { const checks = { } ; for ( const location of toArray ( env . STORAGE_LOCATIONS ) ) { const disk = storage . disk ( location ) ; checks [ `storage:${ location }:responseTime` ] = [ { status : 'ok' , componentType : 'objectstore' , observedValue : 0 , observedUnit : 'ms' , threshold : 750 , } , ] ; const startTime = performance . now ( ) ; try { await disk . put ( `health-${ checkID }` , 'check' ) ; await disk . get ( `health-${ checkID }` ) ; await disk . delete ( `health-${ checkID }` ) ; } catch ( err ) { checks [ `storage:${ location }:responseTime` ] [ 0 ] . status = 'error' ; checks [ `storage:${ location }:responseTime` ] [ 0 ] . output = err ; } finally { const endTime = performance . now ( ) ; checks [ `storage:${ location }:responseTime` ] [ 0 ] . observedValue = + ( endTime - startTime ) . toFixed ( 3 ) ; if ( checks [ `storage:${ location }:responseTime` ] [ 0 ] . observedValue ! > checks [ `storage:${ location }:responseTime` ] [ 0 ] . threshold ! && checks [ `storage:${ location }:responseTime` ] [ 0 ] . status !== 'error' ) { checks [ `storage:${ location }:responseTime` ] [ 0 ] . status = 'warn' ; } } } return checks ; } async function testEmail ( ) { const checks = { 'email:connection' : [ { status : 'ok' , componentType : 'email' , } , ] , } ; const mailer = getMailer ( ) ; try { await mailer . verify ( ) ; } catch ( err ) { checks [ 'email:connection' ] [ 0 ] . status = 'error' ; checks [ 'email:connection' ] [ 0 ] . output = err ; } return checks ; } } }