Add configuration directive for the maximum log files size
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
1 import fs from 'fs';
2 import path from 'path';
3 import { fileURLToPath } from 'url';
4
5 import chalk from 'chalk';
6 import { WorkerChoiceStrategies } from 'poolifier';
7
8 import {
9 type ConfigurationData,
10 type StationTemplateUrl,
11 type StorageConfiguration,
12 SupervisionUrlDistribution,
13 type UIServerConfiguration,
14 type WorkerConfiguration,
15 } from '../types/ConfigurationData';
16 import type { EmptyObject } from '../types/EmptyObject';
17 import type { HandleErrorParams } from '../types/Error';
18 import { FileType } from '../types/FileType';
19 import { StorageType } from '../types/Storage';
20 import { ApplicationProtocol } from '../types/UIProtocol';
21 import { WorkerProcessType } from '../types/Worker';
22 import WorkerConstants from '../worker/WorkerConstants';
23 import Constants from './Constants';
24
25 export default class Configuration {
26 private static configurationFile = path.join(
27 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
28 'assets',
29 'config.json'
30 );
31
32 private static configurationFileWatcher: fs.FSWatcher;
33 private static configuration: ConfigurationData | null = null;
34 private static configurationChangeCallback: () => Promise<void>;
35
36 private constructor() {
37 // This is intentional
38 }
39
40 static setConfigurationChangeCallback(cb: () => Promise<void>): void {
41 Configuration.configurationChangeCallback = cb;
42 }
43
44 static getLogStatisticsInterval(): number {
45 Configuration.warnDeprecatedConfigurationKey(
46 'statisticsDisplayInterval',
47 null,
48 "Use 'logStatisticsInterval' instead"
49 );
50 // Read conf
51 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logStatisticsInterval')
52 ? Configuration.getConfig().logStatisticsInterval
53 : Constants.DEFAULT_LOG_STATISTICS_INTERVAL;
54 }
55
56 static getUIServer(): UIServerConfiguration {
57 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'uiWebSocketServer')) {
58 console.error(
59 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead}`
60 );
61 }
62 let uiServerConfiguration: UIServerConfiguration = {
63 enabled: false,
64 type: ApplicationProtocol.WS,
65 options: {
66 host: Constants.DEFAULT_UI_SERVER_HOST,
67 port: Constants.DEFAULT_UI_SERVER_PORT,
68 },
69 };
70 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'uiServer')) {
71 uiServerConfiguration = Configuration.deepMerge(
72 uiServerConfiguration,
73 Configuration.getConfig().uiServer
74 );
75 }
76 if (Configuration.isCFEnvironment() === true) {
77 delete uiServerConfiguration.options.host;
78 uiServerConfiguration.options.port = parseInt(process.env.PORT);
79 }
80 return uiServerConfiguration;
81 }
82
83 static getPerformanceStorage(): StorageConfiguration {
84 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
85 let storageConfiguration: StorageConfiguration = {
86 enabled: false,
87 type: StorageType.JSON_FILE,
88 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
89 };
90 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'performanceStorage')) {
91 storageConfiguration = {
92 ...storageConfiguration,
93 ...Configuration.getConfig().performanceStorage,
94 };
95 }
96 return storageConfiguration;
97 }
98
99 static getAutoReconnectMaxRetries(): number {
100 Configuration.warnDeprecatedConfigurationKey(
101 'autoReconnectTimeout',
102 null,
103 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
104 );
105 Configuration.warnDeprecatedConfigurationKey(
106 'connectionTimeout',
107 null,
108 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
109 );
110 Configuration.warnDeprecatedConfigurationKey(
111 'autoReconnectMaxRetries',
112 null,
113 'Use it in charging station template instead'
114 );
115 // Read conf
116 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
117 return Configuration.getConfig().autoReconnectMaxRetries;
118 }
119 }
120
121 static getStationTemplateUrls(): StationTemplateUrl[] {
122 Configuration.warnDeprecatedConfigurationKey(
123 'stationTemplateURLs',
124 null,
125 "Use 'stationTemplateUrls' instead"
126 );
127 !Configuration.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
128 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
129 'stationTemplateURLs'
130 ] as StationTemplateUrl[]);
131 Configuration.getConfig().stationTemplateUrls.forEach((stationUrl: StationTemplateUrl) => {
132 if (!Configuration.isUndefined(stationUrl['numberOfStation'])) {
133 console.error(
134 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key 'numberOfStation' usage for template file '${
135 stationUrl.file
136 }' in 'stationTemplateUrls'. Use 'numberOfStations' instead}`
137 );
138 }
139 });
140 // Read conf
141 return Configuration.getConfig().stationTemplateUrls;
142 }
143
144 static getWorker(): WorkerConfiguration {
145 Configuration.warnDeprecatedConfigurationKey(
146 'useWorkerPool',
147 null,
148 "Use 'worker' section to define the type of worker process model instead"
149 );
150 Configuration.warnDeprecatedConfigurationKey(
151 'workerProcess',
152 null,
153 "Use 'worker' section to define the type of worker process model instead"
154 );
155 Configuration.warnDeprecatedConfigurationKey(
156 'workerStartDelay',
157 null,
158 "Use 'worker' section to define the worker start delay instead"
159 );
160 Configuration.warnDeprecatedConfigurationKey(
161 'chargingStationsPerWorker',
162 null,
163 "Use 'worker' section to define the number of element(s) per worker instead"
164 );
165 Configuration.warnDeprecatedConfigurationKey(
166 'elementStartDelay',
167 null,
168 "Use 'worker' section to define the worker's element start delay instead"
169 );
170 Configuration.warnDeprecatedConfigurationKey(
171 'workerPoolMinSize',
172 null,
173 "Use 'worker' section to define the worker pool minimum size instead"
174 );
175 Configuration.warnDeprecatedConfigurationKey(
176 'workerPoolSize;',
177 null,
178 "Use 'worker' section to define the worker pool maximum size instead"
179 );
180 Configuration.warnDeprecatedConfigurationKey(
181 'workerPoolMaxSize;',
182 null,
183 "Use 'worker' section to define the worker pool maximum size instead"
184 );
185 Configuration.warnDeprecatedConfigurationKey(
186 'workerPoolStrategy;',
187 null,
188 "Use 'worker' section to define the worker pool strategy instead"
189 );
190 let workerConfiguration: WorkerConfiguration = {
191 processType: Configuration.objectHasOwnProperty(Configuration.getConfig(), 'workerProcess')
192 ? Configuration.getConfig().workerProcess
193 : WorkerProcessType.WORKER_SET,
194 startDelay: Configuration.objectHasOwnProperty(Configuration.getConfig(), 'workerStartDelay')
195 ? Configuration.getConfig().workerStartDelay
196 : WorkerConstants.DEFAULT_WORKER_START_DELAY,
197 elementsPerWorker: Configuration.objectHasOwnProperty(
198 Configuration.getConfig(),
199 'chargingStationsPerWorker'
200 )
201 ? Configuration.getConfig().chargingStationsPerWorker
202 : WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
203 elementStartDelay: Configuration.objectHasOwnProperty(
204 Configuration.getConfig(),
205 'elementStartDelay'
206 )
207 ? Configuration.getConfig().elementStartDelay
208 : WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
209 poolMinSize: Configuration.objectHasOwnProperty(
210 Configuration.getConfig(),
211 'workerPoolMinSize'
212 )
213 ? Configuration.getConfig().workerPoolMinSize
214 : WorkerConstants.DEFAULT_POOL_MIN_SIZE,
215 poolMaxSize: Configuration.objectHasOwnProperty(
216 Configuration.getConfig(),
217 'workerPoolMaxSize'
218 )
219 ? Configuration.getConfig().workerPoolMaxSize
220 : WorkerConstants.DEFAULT_POOL_MAX_SIZE,
221 poolStrategy:
222 Configuration.getConfig().workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
223 };
224 if (Configuration.objectHasOwnProperty(Configuration.getConfig(), 'worker')) {
225 workerConfiguration = { ...workerConfiguration, ...Configuration.getConfig().worker };
226 }
227 return workerConfiguration;
228 }
229
230 static getLogConsole(): boolean {
231 Configuration.warnDeprecatedConfigurationKey('consoleLog', null, "Use 'logConsole' instead");
232 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logConsole')
233 ? Configuration.getConfig().logConsole
234 : false;
235 }
236
237 static getLogFormat(): string {
238 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFormat')
239 ? Configuration.getConfig().logFormat
240 : 'simple';
241 }
242
243 static getLogRotate(): boolean {
244 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logRotate')
245 ? Configuration.getConfig().logRotate
246 : true;
247 }
248
249 static getLogMaxFiles(): number | string | undefined {
250 return (
251 Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logMaxFiles') &&
252 Configuration.getConfig().logMaxFiles
253 );
254 }
255
256 static getLogMaxSize(): number | string | undefined {
257 return (
258 Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logMaxFiles') &&
259 Configuration.getConfig().logMaxSize
260 );
261 }
262
263 static getLogLevel(): string {
264 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logLevel')
265 ? Configuration.getConfig().logLevel.toLowerCase()
266 : 'info';
267 }
268
269 static getLogFile(): string {
270 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logFile')
271 ? Configuration.getConfig().logFile
272 : 'combined.log';
273 }
274
275 static getLogErrorFile(): string {
276 Configuration.warnDeprecatedConfigurationKey('errorFile', null, "Use 'logErrorFile' instead");
277 return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logErrorFile')
278 ? Configuration.getConfig().logErrorFile
279 : 'error.log';
280 }
281
282 static getSupervisionUrls(): string | string[] {
283 Configuration.warnDeprecatedConfigurationKey(
284 'supervisionURLs',
285 null,
286 "Use 'supervisionUrls' instead"
287 );
288 !Configuration.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
289 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()[
290 'supervisionURLs'
291 ] as string[]);
292 // Read conf
293 return Configuration.getConfig().supervisionUrls;
294 }
295
296 static getSupervisionUrlDistribution(): SupervisionUrlDistribution {
297 Configuration.warnDeprecatedConfigurationKey(
298 'distributeStationToTenantEqually',
299 null,
300 "Use 'supervisionUrlDistribution' instead"
301 );
302 Configuration.warnDeprecatedConfigurationKey(
303 'distributeStationsToTenantsEqually',
304 null,
305 "Use 'supervisionUrlDistribution' instead"
306 );
307 return Configuration.objectHasOwnProperty(
308 Configuration.getConfig(),
309 'supervisionUrlDistribution'
310 )
311 ? Configuration.getConfig().supervisionUrlDistribution
312 : SupervisionUrlDistribution.ROUND_ROBIN;
313 }
314
315 private static logPrefix(): string {
316 return new Date().toLocaleString() + ' Simulator configuration |';
317 }
318
319 private static warnDeprecatedConfigurationKey(
320 key: string,
321 sectionName?: string,
322 logMsgToAppend = ''
323 ) {
324 if (
325 sectionName &&
326 !Configuration.isUndefined(Configuration.getConfig()[sectionName]) &&
327 !Configuration.isUndefined(
328 (Configuration.getConfig()[sectionName] as Record<string, unknown>)[key]
329 )
330 ) {
331 console.error(
332 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage in section '${sectionName}'${
333 logMsgToAppend && '. ' + logMsgToAppend
334 }}`
335 );
336 } else if (!Configuration.isUndefined(Configuration.getConfig()[key])) {
337 console.error(
338 chalk`{green ${Configuration.logPrefix()}} {red Deprecated configuration key '${key}' usage${
339 logMsgToAppend && '. ' + logMsgToAppend
340 }}`
341 );
342 }
343 }
344
345 // Read the config file
346 private static getConfig(): ConfigurationData {
347 if (!Configuration.configuration) {
348 try {
349 Configuration.configuration = JSON.parse(
350 fs.readFileSync(Configuration.configurationFile, 'utf8')
351 ) as ConfigurationData;
352 } catch (error) {
353 Configuration.handleFileException(
354 Configuration.logPrefix(),
355 FileType.Configuration,
356 Configuration.configurationFile,
357 error as NodeJS.ErrnoException
358 );
359 }
360 if (!Configuration.configurationFileWatcher) {
361 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
362 }
363 }
364 return Configuration.configuration;
365 }
366
367 private static getConfigurationFileWatcher(): fs.FSWatcher {
368 try {
369 return fs.watch(Configuration.configurationFile, (event, filename): void => {
370 if (filename && event === 'change') {
371 // Nullify to force configuration file reading
372 Configuration.configuration = null;
373 if (!Configuration.isUndefined(Configuration.configurationChangeCallback)) {
374 Configuration.configurationChangeCallback().catch((error) => {
375 throw typeof error === 'string' ? new Error(error) : error;
376 });
377 }
378 }
379 });
380 } catch (error) {
381 Configuration.handleFileException(
382 Configuration.logPrefix(),
383 FileType.Configuration,
384 Configuration.configurationFile,
385 error as NodeJS.ErrnoException
386 );
387 }
388 }
389
390 private static isCFEnvironment(): boolean {
391 return process.env.VCAP_APPLICATION !== undefined;
392 }
393
394 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
395 const SQLiteFileName = `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`;
396 switch (storageType) {
397 case StorageType.JSON_FILE:
398 return `file://${path.join(
399 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
400 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
401 )}`;
402 case StorageType.SQLITE:
403 return `file://${path.join(
404 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../'),
405 SQLiteFileName
406 )}`;
407 default:
408 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
409 }
410 }
411
412 private static isObject(item: unknown): boolean {
413 return item && typeof item === 'object' && Array.isArray(item) === false;
414 }
415
416 private static objectHasOwnProperty(object: unknown, property: string): boolean {
417 return Object.prototype.hasOwnProperty.call(object, property) as boolean;
418 }
419
420 private static isUndefined(obj: unknown): boolean {
421 return typeof obj === 'undefined';
422 }
423
424 private static deepMerge(target: object, ...sources: object[]): object {
425 if (!sources.length) {
426 return target;
427 }
428 const source = sources.shift();
429
430 if (Configuration.isObject(target) && Configuration.isObject(source)) {
431 for (const key in source) {
432 if (Configuration.isObject(source[key])) {
433 if (!target[key]) {
434 Object.assign(target, { [key]: {} });
435 }
436 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
437 Configuration.deepMerge(target[key], source[key]);
438 } else {
439 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
440 Object.assign(target, { [key]: source[key] });
441 }
442 }
443 }
444 return Configuration.deepMerge(target, ...sources);
445 }
446
447 private static handleFileException(
448 logPrefix: string,
449 fileType: FileType,
450 filePath: string,
451 error: NodeJS.ErrnoException,
452 params: HandleErrorParams<EmptyObject> = { throwError: true }
453 ): void {
454 const prefix = logPrefix.length !== 0 ? logPrefix + ' ' : '';
455 if (error.code === 'ENOENT') {
456 console.error(
457 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' not found: '),
458 error
459 );
460 } else if (error.code === 'EEXIST') {
461 console.error(
462 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' already exists: '),
463 error
464 );
465 } else if (error.code === 'EACCES') {
466 console.error(
467 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' access denied: '),
468 error
469 );
470 } else {
471 console.error(
472 chalk.green(prefix) + chalk.red(fileType + ' file ' + filePath + ' error: '),
473 error
474 );
475 }
476 if (params?.throwError) {
477 throw error;
478 }
479 }
480 }