feat: move logging configuration into its own section
[e-mobility-charging-stations-simulator.git] / src / utils / Configuration.ts
CommitLineData
130783a7
JB
1import fs from 'node:fs';
2import path from 'node:path';
3import { fileURLToPath } from 'node:url';
8114d10e
JB
4
5import chalk from 'chalk';
088ee3c1 6import merge from 'just-merge';
156cab2c 7import { WorkerChoiceStrategies } from 'poolifier';
8114d10e 8
878e026c 9import { Constants } from './Constants';
878e026c 10import { Utils } from './Utils';
83e00df1 11import {
268a74bb 12 ApplicationProtocol,
83e00df1 13 type ConfigurationData,
268a74bb 14 FileType,
3d48c1c1 15 type LogConfiguration,
83e00df1
JB
16 type StationTemplateUrl,
17 type StorageConfiguration,
268a74bb 18 StorageType,
e7aeea18 19 SupervisionUrlDistribution,
83e00df1
JB
20 type UIServerConfiguration,
21 type WorkerConfiguration,
268a74bb
JB
22} from '../types';
23import { WorkerConstants, WorkerProcessType } from '../worker';
7dde0b73 24
268a74bb 25export class Configuration {
a95873d8 26 private static configurationFile = path.join(
51022aa0 27 path.dirname(fileURLToPath(import.meta.url)),
e7aeea18
JB
28 'assets',
29 'config.json'
30 );
10068088 31
1895299d 32 private static configurationFileWatcher: fs.FSWatcher | undefined;
6e0964c8 33 private static configuration: ConfigurationData | null = null;
e57acf6a
JB
34 private static configurationChangeCallback: () => Promise<void>;
35
d5bd1c00
JB
36 private constructor() {
37 // This is intentional
38 }
39
aa7d6d95 40 public static setConfigurationChangeCallback(cb: () => Promise<void>): void {
e57acf6a
JB
41 Configuration.configurationChangeCallback = cb;
42 }
7dde0b73 43
aa7d6d95 44 public static getUIServer(): UIServerConfiguration {
ef4932d8 45 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiWebSocketServer')) {
66271092 46 console.error(
c5e52a07
JB
47 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
48 "Deprecated configuration section 'uiWebSocketServer' usage. Use 'uiServer' instead"
49 )}`
66271092
JB
50 );
51 }
675fa8e3 52 let uiServerConfiguration: UIServerConfiguration = {
b803eefa 53 enabled: false,
1f7fa4de 54 type: ApplicationProtocol.WS,
c127bd64 55 options: {
adbddcb4
JB
56 host: Constants.DEFAULT_UI_SERVER_HOST,
57 port: Constants.DEFAULT_UI_SERVER_PORT,
c127bd64 58 },
6a49ad23 59 };
ef4932d8 60 if (Utils.hasOwnProp(Configuration.getConfig(), 'uiServer')) {
598c886d
JB
61 uiServerConfiguration = merge<UIServerConfiguration>(
62 uiServerConfiguration,
63 Configuration.getConfig()?.uiServer
64 );
6a49ad23 65 }
dada83ec 66 if (Utils.isCFEnvironment() === true) {
72092cfc 67 delete uiServerConfiguration.options?.host;
b803eefa
JB
68 uiServerConfiguration.options.port = parseInt(process.env.PORT);
69 }
675fa8e3 70 return uiServerConfiguration;
6a49ad23
JB
71 }
72
aa7d6d95 73 public static getPerformanceStorage(): StorageConfiguration {
e7aeea18 74 Configuration.warnDeprecatedConfigurationKey('URI', 'performanceStorage', "Use 'uri' instead");
6a49ad23
JB
75 let storageConfiguration: StorageConfiguration = {
76 enabled: false,
77 type: StorageType.JSON_FILE,
e7aeea18 78 uri: this.getDefaultPerformanceStorageUri(StorageType.JSON_FILE),
6a49ad23 79 };
ef4932d8 80 if (Utils.hasOwnProp(Configuration.getConfig(), 'performanceStorage')) {
e7aeea18 81 storageConfiguration = {
1ba1e8fb 82 ...storageConfiguration,
1895299d 83 ...Configuration.getConfig()?.performanceStorage,
f682b2dc
JB
84 ...(Configuration.getConfig()?.performanceStorage?.type === StorageType.JSON_FILE &&
85 Configuration.getConfig()?.performanceStorage?.uri && {
e8044a69 86 uri: Configuration.buildPerformanceUriFilePath(
f682b2dc 87 new URL(Configuration.getConfig()?.performanceStorage?.uri).pathname
e8044a69 88 ),
f682b2dc 89 }),
72f041bd 90 };
72f041bd
JB
91 }
92 return storageConfiguration;
7dde0b73
JB
93 }
94
aa7d6d95 95 public static getAutoReconnectMaxRetries(): number | undefined {
e7aeea18
JB
96 Configuration.warnDeprecatedConfigurationKey(
97 'autoReconnectTimeout',
1895299d 98 undefined,
e7aeea18
JB
99 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
100 );
101 Configuration.warnDeprecatedConfigurationKey(
102 'connectionTimeout',
1895299d 103 undefined,
e7aeea18
JB
104 "Use 'ConnectionTimeOut' OCPP parameter in charging station template instead"
105 );
106 Configuration.warnDeprecatedConfigurationKey(
107 'autoReconnectMaxRetries',
1895299d 108 undefined,
e7aeea18
JB
109 'Use it in charging station template instead'
110 );
7dde0b73 111 // Read conf
ef4932d8 112 if (Utils.hasOwnProp(Configuration.getConfig(), 'autoReconnectMaxRetries')) {
1895299d 113 return Configuration.getConfig()?.autoReconnectMaxRetries;
3574dfd3 114 }
7dde0b73
JB
115 }
116
aa7d6d95 117 public static getStationTemplateUrls(): StationTemplateUrl[] | undefined {
e7aeea18
JB
118 Configuration.warnDeprecatedConfigurationKey(
119 'stationTemplateURLs',
1895299d 120 undefined,
e7aeea18
JB
121 "Use 'stationTemplateUrls' instead"
122 );
dada83ec 123 !Utils.isUndefined(Configuration.getConfig()['stationTemplateURLs']) &&
e7aeea18
JB
124 (Configuration.getConfig().stationTemplateUrls = Configuration.getConfig()[
125 'stationTemplateURLs'
617cad0c 126 ] as StationTemplateUrl[]);
7436ee0d
JB
127 Configuration.getConfig().stationTemplateUrls.forEach(
128 (stationTemplateUrl: StationTemplateUrl) => {
129 if (!Utils.isUndefined(stationTemplateUrl['numberOfStation'])) {
130 console.error(
c5e52a07
JB
131 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
132 `Deprecated configuration key 'numberOfStation' usage for template file '${stationTemplateUrl.file}' in 'stationTemplateUrls'. Use 'numberOfStations' instead`
133 )}`
7436ee0d
JB
134 );
135 }
eb3937cb 136 }
7436ee0d 137 );
7dde0b73 138 // Read conf
1895299d 139 return Configuration.getConfig()?.stationTemplateUrls;
7dde0b73
JB
140 }
141
3d48c1c1
JB
142 public static getLog(): LogConfiguration {
143 Configuration.warnDeprecatedConfigurationKey(
144 'logEnabled',
145 undefined,
146 "Use 'log' section to define the logging enablement instead"
147 );
148 Configuration.warnDeprecatedConfigurationKey(
149 'logFile',
150 undefined,
151 "Use 'log' section to define the log file instead"
152 );
153 Configuration.warnDeprecatedConfigurationKey(
154 'logErrorFile',
155 undefined,
156 "Use 'log' section to define the log error file instead"
157 );
158 Configuration.warnDeprecatedConfigurationKey(
159 'logConsole',
160 undefined,
161 "Use 'log' section to define the console logging enablement instead"
162 );
163 Configuration.warnDeprecatedConfigurationKey(
164 'logStatisticsInterval',
165 undefined,
166 "Use 'log' section to define the log statistics interval instead"
167 );
168 Configuration.warnDeprecatedConfigurationKey(
169 'logLevel',
170 undefined,
171 "Use 'log' section to define the log level instead"
172 );
173 Configuration.warnDeprecatedConfigurationKey(
174 'logFormat',
175 undefined,
176 "Use 'log' section to define the log format instead"
177 );
178 Configuration.warnDeprecatedConfigurationKey(
179 'logRotate',
180 undefined,
181 "Use 'log' section to define the log rotation instead"
182 );
183 Configuration.warnDeprecatedConfigurationKey(
184 'logMaxFiles',
185 undefined,
186 "Use 'log' section to define the log maximum files instead"
187 );
188 Configuration.warnDeprecatedConfigurationKey(
189 'logMaxSize',
190 undefined,
191 "Use 'log' section to define the log maximum size instead"
192 );
193 const defaultLogConfiguration: LogConfiguration = {
194 enabled: true,
195 file: 'logs/combined.log',
196 errorFile: 'logs/error.log',
197 statisticsInterval: Constants.DEFAULT_LOG_STATISTICS_INTERVAL,
198 level: 'info',
199 format: 'simple',
200 rotate: true,
201 };
202 const deprecatedLogConfiguration: LogConfiguration = {
203 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logEnabled') && {
204 enabled: Configuration.getConfig()?.logEnabled,
205 }),
206 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logFile') && {
207 file: Configuration.getConfig()?.logFile,
208 }),
209 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logErrorFile') && {
210 errorFile: Configuration.getConfig()?.logErrorFile,
211 }),
212 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logStatisticsInterval') && {
213 statisticsInterval: Configuration.getConfig()?.logStatisticsInterval,
214 }),
215 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logLevel') && {
216 level: Configuration.getConfig()?.logLevel,
217 }),
218 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logConsole') && {
219 console: Configuration.getConfig()?.logConsole,
220 }),
221 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logFormat') && {
222 format: Configuration.getConfig()?.logFormat,
223 }),
224 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logRotate') && {
225 rotate: Configuration.getConfig()?.logRotate,
226 }),
227 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logMaxFiles') && {
228 maxFiles: Configuration.getConfig()?.logMaxFiles,
229 }),
230 ...(Utils.hasOwnProp(Configuration.getConfig(), 'logMaxSize') && {
231 maxSize: Configuration.getConfig()?.logMaxSize,
232 }),
233 };
234 const logConfiguration: LogConfiguration = {
235 ...defaultLogConfiguration,
236 ...deprecatedLogConfiguration,
237 ...Configuration.getConfig()?.log,
238 };
239 return logConfiguration;
240 }
241
aa7d6d95 242 public static getWorker(): WorkerConfiguration {
e7aeea18 243 Configuration.warnDeprecatedConfigurationKey(
e80bc579 244 'useWorkerPool',
1895299d 245 undefined,
cf2a5d9b
JB
246 "Use 'worker' section to define the type of worker process model instead"
247 );
248 Configuration.warnDeprecatedConfigurationKey(
249 'workerProcess',
1895299d 250 undefined,
cf2a5d9b
JB
251 "Use 'worker' section to define the type of worker process model instead"
252 );
253 Configuration.warnDeprecatedConfigurationKey(
254 'workerStartDelay',
1895299d 255 undefined,
cf2a5d9b
JB
256 "Use 'worker' section to define the worker start delay instead"
257 );
258 Configuration.warnDeprecatedConfigurationKey(
259 'chargingStationsPerWorker',
1895299d 260 undefined,
cf2a5d9b
JB
261 "Use 'worker' section to define the number of element(s) per worker instead"
262 );
263 Configuration.warnDeprecatedConfigurationKey(
264 'elementStartDelay',
1895299d 265 undefined,
cf2a5d9b
JB
266 "Use 'worker' section to define the worker's element start delay instead"
267 );
268 Configuration.warnDeprecatedConfigurationKey(
269 'workerPoolMinSize',
1895299d 270 undefined,
cf2a5d9b 271 "Use 'worker' section to define the worker pool minimum size instead"
e7aeea18 272 );
e7aeea18
JB
273 Configuration.warnDeprecatedConfigurationKey(
274 'workerPoolSize;',
1895299d 275 undefined,
cf2a5d9b 276 "Use 'worker' section to define the worker pool maximum size instead"
e7aeea18 277 );
cf2a5d9b
JB
278 Configuration.warnDeprecatedConfigurationKey(
279 'workerPoolMaxSize;',
1895299d 280 undefined,
cf2a5d9b
JB
281 "Use 'worker' section to define the worker pool maximum size instead"
282 );
283 Configuration.warnDeprecatedConfigurationKey(
284 'workerPoolStrategy;',
1895299d 285 undefined,
cf2a5d9b
JB
286 "Use 'worker' section to define the worker pool strategy instead"
287 );
3d48c1c1
JB
288 const defaultWorkerConfiguration: WorkerConfiguration = {
289 processType: WorkerProcessType.workerSet,
290 startDelay: WorkerConstants.DEFAULT_WORKER_START_DELAY,
291 elementsPerWorker: WorkerConstants.DEFAULT_ELEMENTS_PER_WORKER,
292 elementStartDelay: WorkerConstants.DEFAULT_ELEMENT_START_DELAY,
293 poolMinSize: WorkerConstants.DEFAULT_POOL_MIN_SIZE,
294 poolMaxSize: WorkerConstants.DEFAULT_POOL_MAX_SIZE,
295 poolStrategy: WorkerChoiceStrategies.ROUND_ROBIN,
296 };
297 const deprecatedWorkerConfiguration: WorkerConfiguration = {
298 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerProcess') && {
299 processType: Configuration.getConfig()?.workerProcess,
300 }),
301 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerStartDelay') && {
302 startDelay: Configuration.getConfig()?.workerStartDelay,
303 }),
304 ...(Utils.hasOwnProp(Configuration.getConfig(), 'chargingStationsPerWorker') && {
305 elementsPerWorker: Configuration.getConfig()?.chargingStationsPerWorker,
306 }),
307 ...(Utils.hasOwnProp(Configuration.getConfig(), 'elementStartDelay') && {
308 elementStartDelay: Configuration.getConfig()?.elementStartDelay,
309 }),
310 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMinSize') && {
311 poolMinSize: Configuration.getConfig()?.workerPoolMinSize,
312 }),
313 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolMaxSize') && {
314 poolMaxSize: Configuration.getConfig()?.workerPoolMaxSize,
315 }),
316 ...(Utils.hasOwnProp(Configuration.getConfig(), 'workerPoolStrategy') && {
317 poolStrategy:
318 Configuration.getConfig()?.workerPoolStrategy ?? WorkerChoiceStrategies.ROUND_ROBIN,
319 }),
320 };
321 const workerConfiguration: WorkerConfiguration = {
322 ...defaultWorkerConfiguration,
323 ...deprecatedWorkerConfiguration,
324 ...Configuration.getConfig()?.worker,
cf2a5d9b 325 };
cf2a5d9b 326 return workerConfiguration;
3d2ff9e4
J
327 }
328
aa7d6d95
JB
329 public static workerPoolInUse(): boolean {
330 return [WorkerProcessType.dynamicPool, WorkerProcessType.staticPool].includes(
331 Configuration.getWorker().processType
332 );
333 }
334
335 public static workerDynamicPoolInUse(): boolean {
336 return Configuration.getWorker().processType === WorkerProcessType.dynamicPool;
337 }
338
aa7d6d95 339 public static getSupervisionUrls(): string | string[] | undefined {
e7aeea18
JB
340 Configuration.warnDeprecatedConfigurationKey(
341 'supervisionURLs',
1895299d 342 undefined,
e7aeea18
JB
343 "Use 'supervisionUrls' instead"
344 );
dada83ec 345 !Utils.isUndefined(Configuration.getConfig()['supervisionURLs']) &&
1895299d
JB
346 (Configuration.getConfig().supervisionUrls = Configuration.getConfig()['supervisionURLs'] as
347 | string
348 | string[]);
7dde0b73 349 // Read conf
1895299d 350 return Configuration.getConfig()?.supervisionUrls;
7dde0b73
JB
351 }
352
aa7d6d95 353 public static getSupervisionUrlDistribution(): SupervisionUrlDistribution | undefined {
e7aeea18
JB
354 Configuration.warnDeprecatedConfigurationKey(
355 'distributeStationToTenantEqually',
1895299d 356 undefined,
e7aeea18
JB
357 "Use 'supervisionUrlDistribution' instead"
358 );
359 Configuration.warnDeprecatedConfigurationKey(
360 'distributeStationsToTenantsEqually',
1895299d 361 undefined,
e7aeea18
JB
362 "Use 'supervisionUrlDistribution' instead"
363 );
ef4932d8 364 return Utils.hasOwnProp(Configuration.getConfig(), 'supervisionUrlDistribution')
1895299d 365 ? Configuration.getConfig()?.supervisionUrlDistribution
e7aeea18 366 : SupervisionUrlDistribution.ROUND_ROBIN;
7dde0b73 367 }
eb3937cb 368
8b7072dc 369 private static logPrefix = (): string => {
14ecae6a 370 return `${new Date().toLocaleString()} Simulator configuration |`;
8b7072dc 371 };
23132a44 372
e7aeea18
JB
373 private static warnDeprecatedConfigurationKey(
374 key: string,
375 sectionName?: string,
376 logMsgToAppend = ''
377 ) {
e7aeea18
JB
378 if (
379 sectionName &&
dada83ec
JB
380 !Utils.isUndefined(Configuration.getConfig()[sectionName]) &&
381 !Utils.isUndefined((Configuration.getConfig()[sectionName] as Record<string, unknown>)[key])
e7aeea18
JB
382 ) {
383 console.error(
c5e52a07
JB
384 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
385 `Deprecated configuration key '${key}' usage in section '${sectionName}'${
386 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
387 }`
388 )}`
e7aeea18 389 );
dada83ec 390 } else if (!Utils.isUndefined(Configuration.getConfig()[key])) {
e7aeea18 391 console.error(
c5e52a07
JB
392 `${chalk.green(Configuration.logPrefix())} ${chalk.red(
393 `Deprecated configuration key '${key}' usage${
394 logMsgToAppend.trim().length > 0 ? `. ${logMsgToAppend}` : ''
395 }`
396 )}`
e7aeea18 397 );
eb3937cb
JB
398 }
399 }
400
401 // Read the config file
1895299d 402 private static getConfig(): ConfigurationData | null {
eb3937cb 403 if (!Configuration.configuration) {
23132a44 404 try {
e7aeea18 405 Configuration.configuration = JSON.parse(
a95873d8 406 fs.readFileSync(Configuration.configurationFile, 'utf8')
e7aeea18 407 ) as ConfigurationData;
23132a44 408 } catch (error) {
69074173 409 Configuration.handleFileException(
a95873d8 410 Configuration.configurationFile,
7164966d
JB
411 FileType.Configuration,
412 error as NodeJS.ErrnoException,
69074173 413 Configuration.logPrefix()
e7aeea18 414 );
23132a44 415 }
ded13d97
JB
416 if (!Configuration.configurationFileWatcher) {
417 Configuration.configurationFileWatcher = Configuration.getConfigurationFileWatcher();
418 }
eb3937cb
JB
419 }
420 return Configuration.configuration;
421 }
963ee397 422
1895299d 423 private static getConfigurationFileWatcher(): fs.FSWatcher | undefined {
23132a44 424 try {
a95873d8 425 return fs.watch(Configuration.configurationFile, (event, filename): void => {
8d54dcc0 426 if (filename?.trim().length > 0 && event === 'change') {
3ec10737
JB
427 // Nullify to force configuration file reading
428 Configuration.configuration = null;
dada83ec 429 if (!Utils.isUndefined(Configuration.configurationChangeCallback)) {
72092cfc 430 Configuration.configurationChangeCallback().catch((error) => {
dcaf96dc
JB
431 throw typeof error === 'string' ? new Error(error) : error;
432 });
3ec10737 433 }
23132a44
JB
434 }
435 });
436 } catch (error) {
69074173 437 Configuration.handleFileException(
a95873d8 438 Configuration.configurationFile,
7164966d
JB
439 FileType.Configuration,
440 error as NodeJS.ErrnoException,
69074173 441 Configuration.logPrefix()
e7aeea18 442 );
23132a44 443 }
ded13d97
JB
444 }
445
69074173
JB
446 private static handleFileException(
447 file: string,
448 fileType: FileType,
449 error: NodeJS.ErrnoException,
450 logPrefix: string
451 ): void {
452 const prefix = Utils.isNotEmptyString(logPrefix) ? `${logPrefix} ` : '';
453 let logMsg: string;
454 switch (error.code) {
455 case 'ENOENT':
456 logMsg = `${fileType} file ${file} not found:`;
457 break;
458 case 'EEXIST':
459 logMsg = `${fileType} file ${file} already exists:`;
460 break;
461 case 'EACCES':
462 logMsg = `${fileType} file ${file} access denied:`;
463 break;
7b5dbe91
JB
464 case 'EPERM':
465 logMsg = `${fileType} file ${file} permission denied:`;
466 break;
69074173
JB
467 default:
468 logMsg = `${fileType} file ${file} error:`;
469 }
7b5dbe91
JB
470 console.error(`${chalk.green(prefix)}${chalk.red(`${logMsg} `)}`, error);
471 throw error;
69074173
JB
472 }
473
1f5df42a 474 private static getDefaultPerformanceStorageUri(storageType: StorageType) {
d5603918
JB
475 switch (storageType) {
476 case StorageType.JSON_FILE:
e8044a69 477 return Configuration.buildPerformanceUriFilePath(
e7aeea18 478 Constants.DEFAULT_PERFORMANCE_RECORDS_FILENAME
e8044a69 479 );
d5603918 480 case StorageType.SQLITE:
e8044a69 481 return Configuration.buildPerformanceUriFilePath(
5bcb75d4 482 `${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`
e8044a69 483 );
d5603918
JB
484 default:
485 throw new Error(`Performance storage URI is mandatory with storage type '${storageType}'`);
486 }
487 }
e8044a69
JB
488
489 private static buildPerformanceUriFilePath(file: string) {
490 return `file://${path.join(
51022aa0 491 path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../'),
e8044a69
JB
492 file
493 )}`;
494 }
7dde0b73 495}