From a66bbcfe85550dc01a2e32bd17a52f5980a78193 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Mon, 5 Feb 2024 16:15:24 +0100 Subject: [PATCH] feat: add performance statistics to UI protocol MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- README.md | 16 +++- src/assets/config-template.json | 4 +- ...nia-CSSimulatorUIWSProtocolCollection.json | 80 ++++++++++++++----- .../Insomnia_CSSimulatorUIProtocol.json | 38 ++++++++- src/charging-station/Bootstrap.ts | 4 + .../ui-server/UIHttpServer.ts | 11 ++- .../ui-server/UIWebSocketServer.ts | 3 +- .../ui-services/AbstractUIService.ts | 51 ++++++++++-- src/performance/storage/JsonFileStorage.ts | 9 +-- src/performance/storage/MikroOrmStorage.ts | 2 + src/performance/storage/MongoDBStorage.ts | 2 + src/performance/storage/None.ts | 22 +++++ src/performance/storage/Storage.ts | 13 +++ src/performance/storage/StorageFactory.ts | 4 + src/types/Storage.ts | 1 + src/types/UIProtocol.ts | 1 + src/utils/Configuration.ts | 8 +- src/utils/Utils.ts | 9 ++- 18 files changed, 236 insertions(+), 42 deletions(-) create mode 100644 src/performance/storage/None.ts diff --git a/README.md b/README.md index 89ea7b6c..060d90bc 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ But the modifications to test have to be done to the files in the build target d | log | | {
"enabled": true,
"file": "logs/combined.log",
"errorFile": "logs/error.log",
"statisticsInterval": 60,
"level": "info",
"console": false,
"format": "simple",
"rotate": true
} | {
enabled?: boolean;
file?: string;
errorFile?: string;
statisticsInterval?: number;
level?: string;
console?: boolean;
format?: string;
rotate?: boolean;
maxFiles?: string \| number;
maxSize?: string \| number;
} | Log configuration section:
- _enabled_: enable logging
- _file_: log file relative path
- _errorFile_: error log file relative path
- _statisticsInterval_: seconds between charging stations statistics output in the logs
- _level_: emerg/alert/crit/error/warning/notice/info/debug [winston](https://github.com/winstonjs/winston) logging level
- _console_: output logs on the console
- _format_: [winston](https://github.com/winstonjs/winston) log format
- _rotate_: enable daily log files rotation
- _maxFiles_: maximum number of log files: https://github.com/winstonjs/winston-daily-rotate-file#options
- _maxSize_: maximum size of log files in bytes, or units of kb, mb, and gb: https://github.com/winstonjs/winston-daily-rotate-file#options | | worker | | {
"processType": "workerSet",
"startDelay": 500,
"elementStartDelay": 0,
"elementsPerWorker": 'auto',
"poolMinSize": 4,
"poolMaxSize": 16
} | {
processType?: WorkerProcessType;
startDelay?: number;
elementStartDelay?: number;
elementsPerWorker?: number \| 'auto' \| 'all';
poolMinSize?: number;
poolMaxSize?: number;
resourceLimits?: ResourceLimits;
} | Worker configuration section:
- _processType_: worker threads process type (`workerSet`/`fixedPool`/`dynamicPool`)
- _startDelay_: milliseconds to wait at worker threads startup (only for `workerSet` worker threads process type)
- _elementStartDelay_: milliseconds to wait at charging station startup
- _elementsPerWorker_: number of charging stations per worker threads for the `workerSet` process type (`auto` means (number of stations) / (number of CPUs) \* 1.5 if (number of stations) > (number of CPUs), otherwise 1; `all` means a unique worker will run all charging stations)
- _poolMinSize_: worker threads pool minimum number of threads
- _poolMaxSize_: worker threads pool maximum number of threads
- _resourceLimits_: worker threads [resource limits](https://nodejs.org/api/worker_threads.html#new-workerfilename-options) object option | | uiServer | | {
"enabled": false,
"type": "ws",
"version": "1.1",
"options": {
"host": "localhost",
"port": 8080
}
} | {
enabled?: boolean;
type?: ApplicationProtocol;
version?: ApplicationProtocolVersion;
options?: ServerOptions;
authentication?: {
enabled: boolean;
type: AuthenticationType;
username?: string;
password?: string;
}
} | UI server configuration section:
- _enabled_: enable UI server
- _type_: 'http' or 'ws'
- _version_: HTTP version '1.1' or '2.0'
- _options_: node.js net module [listen options](https://nodejs.org/api/net.html#serverlistenoptions-callback)
- _authentication_: authentication type configuration section | -| performanceStorage | | {
"enabled": false,
"type": "jsonfile",
"uri": "file:///performance/performanceRecords.json"
} | {
enabled?: boolean;
type?: string;
uri?: string;
} | Performance storage configuration section:
- _enabled_: enable performance storage
- _type_: 'jsonfile' or 'mongodb'
- _uri_: storage URI | +| performanceStorage | | {
"enabled": true,
"type": "none",
} | {
enabled?: boolean;
type?: string;
uri?: string;
} | Performance storage configuration section:
- _enabled_: enable performance storage
- _type_: 'jsonfile', 'mongodb' or 'none'
- _uri_: storage URI | | stationTemplateUrls | | {}[] | {
file: string;
numberOfStations: number;
}[] | array of charging station configuration templates URIs configuration section (charging station configuration template file name and number of stations) | #### Worker process model @@ -585,7 +585,7 @@ Set the Websocket header _Sec-Websocket-Protocol_ to `ui0.0.1`. `ProcedureName`: 'setSupervisionUrl' `PDU`: { `hashIds`: charging station unique identifier strings array (optional, default: all charging stations), - `uri`: string + `url`: string } - Response: @@ -596,6 +596,18 @@ Set the Websocket header _Sec-Websocket-Protocol_ to `ui0.0.1`. `responsesFailed`: failed responses payload array (optional) } +###### Performance Statistics + +- Request: + `ProcedureName`: 'performanceStatistics' + `PDU`: {} + +- Response: + `PDU`: { + `status`: 'success' | 'failure' + `performanceStatistics`: Statistics[] + } + ###### List Charging Stations - Request: diff --git a/src/assets/config-template.json b/src/assets/config-template.json index 5e18d942..641117ea 100644 --- a/src/assets/config-template.json +++ b/src/assets/config-template.json @@ -11,8 +11,8 @@ "processType": "workerSet" }, "performanceStorage": { - "enabled": false, - "type": "jsonfile" + "enabled": true, + "type": "none" }, "uiServer": { "enabled": false, diff --git a/src/assets/ui-protocol/Insomnia-CSSimulatorUIWSProtocolCollection.json b/src/assets/ui-protocol/Insomnia-CSSimulatorUIWSProtocolCollection.json index 07738fb1..f9661ed9 100644 --- a/src/assets/ui-protocol/Insomnia-CSSimulatorUIWSProtocolCollection.json +++ b/src/assets/ui-protocol/Insomnia-CSSimulatorUIWSProtocolCollection.json @@ -1,7 +1,7 @@ { "_type": "export", "__export_format": 4, - "__export_date": "2024-02-01T10:00:36.181Z", + "__export_date": "2024-02-05T15:02:32.625Z", "__export_source": "insomnia.desktop.app:v8.6.0", "resources": [ { @@ -238,6 +238,38 @@ "description": "", "_type": "websocket_request" }, + { + "_id": "ws-req_0bab7a97ceda4944976a463f616dec5c", + "parentId": "wrk_64c9d5670f014930baf668326b95e601", + "modified": 1707143784125, + "created": 1707143784125, + "name": "performanceStatistics", + "url": "{{ _.baseUrl }}", + "metaSortKey": -1671191988790.125, + "headers": [ + { + "id": "pair_9a64d3b0bc654ab68710ef138f00d3f5", + "name": "Sec-WebSocket-Protocol", + "value": "{{ _.protocol }}{{ _.version }}", + "description": "" + } + ], + "authentication": { + "type": "basic", + "useISO88591": false, + "disabled": false, + "username": "{{ _.username }}", + "password": "{{ _.password }}" + }, + "parameters": [], + "pathParameters": [], + "settingEncodeUrl": true, + "settingStoreCookies": true, + "settingSendCookies": true, + "settingFollowRedirects": "global", + "description": "", + "_type": "websocket_request" + }, { "_id": "ws-req_ebe5a555a6344dfba7e29f857af11d08", "parentId": "wrk_64c9d5670f014930baf668326b95e601", @@ -602,7 +634,7 @@ { "_id": "ws-payload_5a7ab577051646ff9975c34ccf900f18", "parentId": "ws-req_6154d7eed8ba498ca6da5245e205a329", - "modified": 1706781503850, + "modified": 1707145313292, "created": 1671192074985, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"stopSimulator\",\n\t{}\n]", @@ -612,7 +644,7 @@ { "_id": "ws-payload_b6275ce690b5411eb84265642cda2014", "parentId": "ws-req_6815f92a40cf410383b99302180164f6", - "modified": 1706781539872, + "modified": 1707145312464, "created": 1671297215182, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"startSimulator\",\n\t{}\n]", @@ -622,7 +654,7 @@ { "_id": "ws-payload_19c357dbe2fd4925aa43f58726af8b36", "parentId": "ws-req_cd2c7d152f834abea3def6f66c63c7e2", - "modified": 1706781504424, + "modified": 1707145314106, "created": 1671297360441, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"openConnection\",\n\t{}\n]", @@ -632,7 +664,7 @@ { "_id": "ws-payload_c61ab0e9f9ee43fe96782dfbeabf97d2", "parentId": "ws-req_8f579f886db842118bf0e1835e7aa750", - "modified": 1706780850139, + "modified": 1707145314837, "created": 1671297412505, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"closeConnection\",\n\t{}\n]", @@ -642,7 +674,7 @@ { "_id": "ws-payload_2872e2656c164769acf98cc7ba7ea028", "parentId": "ws-req_e5902850ac1d40369bd6e942a2755a9d", - "modified": 1706781508144, + "modified": 1707145316242, "created": 1671297544207, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"listChargingStations\",\n\t{}\n]", @@ -652,7 +684,7 @@ { "_id": "ws-payload_edebda0226aa43f88712d7feb60ac645", "parentId": "ws-req_ebe5a555a6344dfba7e29f857af11d08", - "modified": 1706781518372, + "modified": 1707145319096, "created": 1671297697172, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"startChargingStation\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"5b82a559d2b453f6277e272e134ae824ae358cfb6ee2415af9f7c2f325ef8b3e930aeeadcd866df4b8aec58786e60ae7\"\n\t\t]\n\t}\n]", @@ -662,7 +694,7 @@ { "_id": "ws-payload_20cb03a0142d44a98ddb7bc59ccfea11", "parentId": "ws-req_720b5d562a0f42929ef9aa16019728fe", - "modified": 1706781522084, + "modified": 1707145319951, "created": 1671297731073, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"stopChargingStation\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"5b82a559d2b453f6277e272e134ae824ae358cfb6ee2415af9f7c2f325ef8b3e930aeeadcd866df4b8aec58786e60ae7\"\n\t\t]\n\t}\n]", @@ -672,7 +704,7 @@ { "_id": "ws-payload_d8e66e0f933e4d74bb5fbff4d15a44bf", "parentId": "ws-req_23025e078480491daf01406b2b5e9cc2", - "modified": 1706781522633, + "modified": 1707145321035, "created": 1671298432039, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"startAutomaticTransactionGenerator\",\n\t{}\n]", @@ -682,7 +714,7 @@ { "_id": "ws-payload_4f502389fb3348ee9dcaf3419fbc49ba", "parentId": "ws-req_bdbae9eeb408489ca8e3283b437ccbf4", - "modified": 1706781524682, + "modified": 1707145321730, "created": 1671298535001, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"stopAutomaticTransactionGenerator\",\n\t{}\n]", @@ -692,7 +724,7 @@ { "_id": "ws-payload_1b02fae03f9c4f54af23911678519841", "parentId": "ws-req_f836c127aca54a909a110a16b791c29b", - "modified": 1706781536669, + "modified": 1707145326651, "created": 1673277254287, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"diagnosticsStatusNotification\",\n\t{\n\t\t\"status\": \"Uploaded\"\n\t}\n]", @@ -702,7 +734,7 @@ { "_id": "ws-payload_b563d5d8dc284ebb8f9dd2083734cc45", "parentId": "ws-req_c326f21a473c430081d7229a82c69b33", - "modified": 1706781537962, + "modified": 1707145327532, "created": 1673279189375, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"firmwareStatusNotification\",\n\t{\n\t\t\"status\": \"Downloading\"\n\t}\n]", @@ -712,7 +744,7 @@ { "_id": "ws-payload_e2e7b8a7d8694b94a16868fcd0b90916", "parentId": "ws-req_d88784511f704224999e41bd53ba71b8", - "modified": 1706781534291, + "modified": 1707145325829, "created": 1673728879079, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"statusNotification\",\n\t{\n\t\t\"connectorId\": 1,\n\t\t\"status\": \"Available\",\n\t\t\"errorCode\": \"NoError\"\n\t}\n]", @@ -722,7 +754,7 @@ { "_id": "ws-payload_bf36aa8e9c8646d6a37697c6496e257f", "parentId": "ws-req_fc239903df2d46bb998c16dbcb8cafea", - "modified": 1706781527560, + "modified": 1707145324132, "created": 1674411426307, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"startTransaction\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"0058d8b50e422cce5bbd0c0a4ad13d5d657e8a88670dcf04c1b2b563fea3db5b96a3686278b374ed050e21baef89060e\"\n\t\t],\n\t\t\"connectorId\": 1,\n\t\t\"idTag\": \"test\"\n\t}\n]", @@ -732,7 +764,7 @@ { "_id": "ws-payload_43c713bdb0e64cbda34b3102f42da321", "parentId": "ws-req_e3db8f3f31c947c1969a6a257b65a2d5", - "modified": 1706781531466, + "modified": 1707144801240, "created": 1674411483206, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"stopTransaction\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"0058d8b50e422cce5bbd0c0a4ad13d5d657e8a88670dcf04c1b2b563fea3db5b96a3686278b374ed050e21baef89060e\"\n\t\t],\n\t\t\"transactionId\": 235051179\n\t}\n]", @@ -742,17 +774,17 @@ { "_id": "ws-payload_95c28d71c8d940bb83ac514f8916a66d", "parentId": "ws-req_afbfa6e6824b427e99e735c0b1eabe3b", - "modified": 1706781526291, + "modified": 1707145322952, "created": 1678991663554, "name": "New Payload", - "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"setSupervisionUrl\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"31f796168755b5dec7114fddebf951fcf37dd9c72e3f511fec5a86148be057ff239a57a51e65d254ec456dcd22f0df5a\"\n\t\t],\n\t\t\"url\": \"wss://ev-ocpp-json-server-plugncharge.cfapps.eu12.hana.ondemand.com/OCPP16/1839eb8b-b05e-49d5-bfff-04426b24834b/MBXDMg2i/6ceb0ecc\"\n\t}\n]", + "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"setSupervisonUrl\",\n\t{\n\t\t\"hashIds\": [\n\t\t\t\"5b82a559d2b453f6277e272e134ae824ae358cfb6ee2415af9f7c2f325ef8b3e930aeeadcd866df4b8aec58786e60ae7\"\n\t\t],\n\t\t\"url\": \"wss://domain.tld\"\n\t}\n]", "mode": "application/json", "_type": "websocket_payload" }, { "_id": "ws-payload_3e1dffbcefcc481286b44c694b9e6496", "parentId": "ws-req_3a0ff14878b449f4be3dfbb7432b5f87", - "modified": 1706781510837, + "modified": 1707145315238, "created": 1706726300041, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"listTemplates\",\n\t{}\n]", @@ -762,12 +794,22 @@ { "_id": "ws-payload_5e2dfed34a104c28b887c885ada1b4af", "parentId": "ws-req_8777c5635dd64fccbc2b0f450be656c0", - "modified": 1706781515138, + "modified": 1707145316885, "created": 1706778795544, "name": "New Payload", "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"addChargingStations\",\n\t{\n\t\t\"template\": \"evlink.station-template\",\n\t\t\"numberOfStations\": 1\n\t}\n]", "mode": "application/json", "_type": "websocket_payload" + }, + { + "_id": "ws-payload_494fba679fa644ccb318f092e780834f", + "parentId": "ws-req_0bab7a97ceda4944976a463f616dec5c", + "modified": 1707145317588, + "created": 1707143784130, + "name": "New Payload", + "value": "[\n\t\"{% uuid 'v4' %}\",\n\t\"performanceStatistics\",\n\t{}\n]", + "mode": "application/json", + "_type": "websocket_payload" } ] } diff --git a/src/assets/ui-protocol/Insomnia_CSSimulatorUIProtocol.json b/src/assets/ui-protocol/Insomnia_CSSimulatorUIProtocol.json index 413123a0..43c9a306 100644 --- a/src/assets/ui-protocol/Insomnia_CSSimulatorUIProtocol.json +++ b/src/assets/ui-protocol/Insomnia_CSSimulatorUIProtocol.json @@ -1,7 +1,7 @@ { "_type": "export", "__export_format": 4, - "__export_date": "2024-02-01T10:00:15.210Z", + "__export_date": "2024-02-05T15:10:03.585Z", "__export_source": "insomnia.desktop.app:v8.6.0", "resources": [ { @@ -197,6 +197,42 @@ "settingFollowRedirects": "global", "_type": "request" }, + { + "_id": "req_eb5ab162f6d64b87b61968263e743309", + "parentId": "wrk_509d4a5094fa485ba93e53bc735e8ac3", + "modified": 1707145606591, + "created": 1707145585253, + "url": "{{baseUrl}}/{{protocol}}/{{version}}/performanceStatistics", + "name": "performanceStatistics", + "description": "", + "method": "POST", + "body": { "mimeType": "application/json", "text": "{}" }, + "parameters": [], + "headers": [ + { + "name": "Content-Type", + "value": "application/json", + "id": "pair_af9f914ca52f407488bc6df6c7db3a08" + } + ], + "authentication": { + "type": "basic", + "useISO88591": false, + "disabled": false, + "username": "{{username}}", + "password": "{{password}}" + }, + "metaSortKey": -999999962.5, + "isPrivate": false, + "pathParameters": [], + "settingStoreCookies": true, + "settingSendCookies": true, + "settingDisableRenderRequestBody": false, + "settingEncodeUrl": true, + "settingRebuildPath": true, + "settingFollowRedirects": "global", + "_type": "request" + }, { "_id": "req_85f61e3486114d52abd4e18f212afd59", "parentId": "wrk_509d4a5094fa485ba93e53bc735e8ac3", diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index f238d951..2c1b0f43 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -120,6 +120,10 @@ export class Bootstrap extends EventEmitter { return this.chargingStationsByTemplate.get(templateName)?.lastIndex ?? 0 } + public getPerformanceStatistics (): IterableIterator | undefined { + return this.storage?.getPerformanceStatistics() + } + private get numberOfAddedChargingStations (): number { return [...this.chargingStationsByTemplate.values()].reduce( (accumulator, value) => accumulator + value.added, diff --git a/src/charging-station/ui-server/UIHttpServer.ts b/src/charging-station/ui-server/UIHttpServer.ts index 7c102195..27e8d7ba 100644 --- a/src/charging-station/ui-server/UIHttpServer.ts +++ b/src/charging-station/ui-server/UIHttpServer.ts @@ -16,7 +16,14 @@ import { ResponseStatus, type UIServerConfiguration } from '../../types/index.js' -import { Constants, generateUUID, isNotEmptyString, logPrefix, logger } from '../../utils/index.js' +import { + Constants, + JSONStringifyWithMapSupport, + generateUUID, + isNotEmptyString, + logPrefix, + logger +} from '../../utils/index.js' const moduleName = 'UIHttpServer' @@ -54,7 +61,7 @@ export class UIHttpServer extends AbstractUIServer { .writeHead(this.responseStatusToStatusCode(payload.status), { 'Content-Type': 'application/json' }) - .end(JSON.stringify(payload)) + .end(JSONStringifyWithMapSupport(payload)) } else { logger.error( `${this.logPrefix(moduleName, 'sendResponse')} Response for unknown request id: ${uuid}` diff --git a/src/charging-station/ui-server/UIWebSocketServer.ts b/src/charging-station/ui-server/UIWebSocketServer.ts index 42162d16..0dce52ca 100644 --- a/src/charging-station/ui-server/UIWebSocketServer.ts +++ b/src/charging-station/ui-server/UIWebSocketServer.ts @@ -14,6 +14,7 @@ import { } from '../../types/index.js' import { Constants, + JSONStringifyWithMapSupport, getWebSocketCloseEventStatusString, isNotEmptyString, logPrefix, @@ -120,7 +121,7 @@ export class UIWebSocketServer extends AbstractUIServer { if (this.hasResponseHandler(responseId)) { const ws = this.responseHandlers.get(responseId) as WebSocket if (ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify(response)) + ws.send(JSONStringifyWithMapSupport(response)) } else { logger.error( `${this.logPrefix( diff --git a/src/charging-station/ui-server/ui-services/AbstractUIService.ts b/src/charging-station/ui-server/ui-services/AbstractUIService.ts index 9d044a09..8617c7ab 100644 --- a/src/charging-station/ui-server/ui-services/AbstractUIService.ts +++ b/src/charging-station/ui-server/ui-services/AbstractUIService.ts @@ -2,6 +2,7 @@ import { BaseError, type OCPPError } from '../../../exception/index.js' import { BroadcastChannelProcedureName, type BroadcastChannelRequestPayload, + ConfigurationSection, type JsonType, ProcedureName, type ProtocolRequest, @@ -10,9 +11,10 @@ import { type ProtocolVersion, type RequestPayload, type ResponsePayload, - ResponseStatus + ResponseStatus, + type StorageConfiguration } from '../../../types/index.js' -import { isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js' +import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js' import { Bootstrap } from '../../Bootstrap.js' import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js' import type { AbstractUIServer } from '../AbstractUIServer.js' @@ -68,6 +70,7 @@ export abstract class AbstractUIService { [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)], [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)], [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)], + [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)], [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)], [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)] ]) @@ -252,12 +255,44 @@ export abstract class AbstractUIService { } } + private handlePerformanceStatistics (): ResponsePayload { + if ( + Configuration.getConfigurationSection( + ConfigurationSection.performanceStorage + ).enabled !== true + ) { + return { + status: ResponseStatus.FAILURE, + errorMessage: 'Performance statistics storage is not enabled' + } satisfies ResponsePayload + } + try { + return { + status: ResponseStatus.SUCCESS, + performanceStatistics: [ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...Bootstrap.getInstance().getPerformanceStatistics()! + ] as JsonType[] + } + } catch (error) { + return { + status: ResponseStatus.FAILURE, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack + } satisfies ResponsePayload + } + } + private async handleStartSimulator (): Promise { try { await Bootstrap.getInstance().start() return { status: ResponseStatus.SUCCESS } - } catch { - return { status: ResponseStatus.FAILURE } + } catch (error) { + return { + status: ResponseStatus.FAILURE, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack + } satisfies ResponsePayload } } @@ -265,8 +300,12 @@ export abstract class AbstractUIService { try { await Bootstrap.getInstance().stop() return { status: ResponseStatus.SUCCESS } - } catch { - return { status: ResponseStatus.FAILURE } + } catch (error) { + return { + status: ResponseStatus.FAILURE, + errorMessage: (error as Error).message, + errorStack: (error as Error).stack + } satisfies ResponsePayload } } } diff --git a/src/performance/storage/JsonFileStorage.ts b/src/performance/storage/JsonFileStorage.ts index 31d18537..a69edd69 100644 --- a/src/performance/storage/JsonFileStorage.ts +++ b/src/performance/storage/JsonFileStorage.ts @@ -14,8 +14,6 @@ import { } from '../../utils/index.js' export class JsonFileStorage extends Storage { - private static performanceRecords: Map - private fd?: number constructor (storageUri: string, logPrefix: string) { @@ -24,13 +22,13 @@ export class JsonFileStorage extends Storage { } public storePerformanceStatistics (performanceStatistics: Statistics): void { + this.setPerformanceStatistics(performanceStatistics) this.checkPerformanceRecordsFile() - JsonFileStorage.performanceRecords.set(performanceStatistics.id, performanceStatistics) AsyncLock.runExclusive(AsyncLockType.performance, () => { writeSync( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion this.fd!, - JSONStringifyWithMapSupport([...JsonFileStorage.performanceRecords.values()], 2), + JSONStringifyWithMapSupport([...this.getPerformanceStatistics()], 2), 0, 'utf8' ) @@ -45,7 +43,6 @@ export class JsonFileStorage extends Storage { } public open (): void { - JsonFileStorage.performanceRecords = new Map() try { if (this.fd == null) { if (!existsSync(dirname(this.dbName))) { @@ -64,7 +61,7 @@ export class JsonFileStorage extends Storage { } public close (): void { - JsonFileStorage.performanceRecords.clear() + this.clearPerformanceStatistics() try { if (this.fd != null) { closeSync(this.fd) diff --git a/src/performance/storage/MikroOrmStorage.ts b/src/performance/storage/MikroOrmStorage.ts index a1cc7c8d..e9169306 100644 --- a/src/performance/storage/MikroOrmStorage.ts +++ b/src/performance/storage/MikroOrmStorage.ts @@ -19,6 +19,7 @@ export class MikroOrmStorage extends Storage { public async storePerformanceStatistics (performanceStatistics: Statistics): Promise { try { + this.setPerformanceStatistics(performanceStatistics) await this.orm?.em.upsert({ ...performanceStatistics, statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({ @@ -50,6 +51,7 @@ export class MikroOrmStorage extends Storage { } public async close (): Promise { + this.clearPerformanceStatistics() try { if (this.orm != null) { await this.orm.close() diff --git a/src/performance/storage/MongoDBStorage.ts b/src/performance/storage/MongoDBStorage.ts index 48ce06d6..b3d535e2 100644 --- a/src/performance/storage/MongoDBStorage.ts +++ b/src/performance/storage/MongoDBStorage.ts @@ -20,6 +20,7 @@ export class MongoDBStorage extends Storage { public async storePerformanceStatistics (performanceStatistics: Statistics): Promise { try { + this.setPerformanceStatistics(performanceStatistics) this.checkDBConnection() await this.client ?.db(this.dbName) @@ -42,6 +43,7 @@ export class MongoDBStorage extends Storage { } public async close (): Promise { + this.clearPerformanceStatistics() try { if (this.connected && this.client != null) { await this.client.close() diff --git a/src/performance/storage/None.ts b/src/performance/storage/None.ts new file mode 100644 index 00000000..6506d36b --- /dev/null +++ b/src/performance/storage/None.ts @@ -0,0 +1,22 @@ +// Copyright Jerome Benoit. 2021-2024. All Rights Reserved. + +import { Storage } from './Storage.js' +import type { Statistics } from '../../types/index.js' + +export class None extends Storage { + constructor () { + super('none://none', 'none') + } + + public storePerformanceStatistics (performanceStatistics: Statistics): void { + this.setPerformanceStatistics(performanceStatistics) + } + + public open (): void { + /** Intentionally empty */ + } + + public close (): void { + this.clearPerformanceStatistics() + } +} diff --git a/src/performance/storage/Storage.ts b/src/performance/storage/Storage.ts index d9539190..aee3ae9e 100644 --- a/src/performance/storage/Storage.ts +++ b/src/performance/storage/Storage.ts @@ -15,6 +15,7 @@ export abstract class Storage { protected readonly storageUri: URL protected readonly logPrefix: string protected dbName!: string + private static readonly performanceStatistics = new Map() constructor (storageUri: string, logPrefix: string) { this.storageUri = new URL(storageUri) @@ -53,6 +54,18 @@ export abstract class Storage { } } + public getPerformanceStatistics (): IterableIterator { + return Storage.performanceStatistics.values() + } + + protected setPerformanceStatistics (performanceStatistics: Statistics): void { + Storage.performanceStatistics.set(performanceStatistics.id, performanceStatistics) + } + + protected clearPerformanceStatistics (): void { + Storage.performanceStatistics.clear() + } + public abstract open (): void | Promise public abstract close (): void | Promise public abstract storePerformanceStatistics ( diff --git a/src/performance/storage/StorageFactory.ts b/src/performance/storage/StorageFactory.ts index 8753bea1..9da6b54e 100644 --- a/src/performance/storage/StorageFactory.ts +++ b/src/performance/storage/StorageFactory.ts @@ -3,6 +3,7 @@ import { JsonFileStorage } from './JsonFileStorage.js' import { MikroOrmStorage } from './MikroOrmStorage.js' import { MongoDBStorage } from './MongoDBStorage.js' +import { None } from './None.js' import type { Storage } from './Storage.js' import { BaseError } from '../../exception/index.js' import { StorageType } from '../../types/index.js' @@ -31,6 +32,9 @@ export class StorageFactory { case StorageType.MYSQL: storageInstance = new MikroOrmStorage(connectionUri, logPrefix, type) break + case StorageType.NONE: + storageInstance = new None() + break default: // eslint-disable-next-line @typescript-eslint/restrict-template-expressions throw new BaseError(`${logPrefix} Unknown storage type: ${type}`) diff --git a/src/types/Storage.ts b/src/types/Storage.ts index 4d4a4e4e..232c9d41 100644 --- a/src/types/Storage.ts +++ b/src/types/Storage.ts @@ -1,4 +1,5 @@ export enum StorageType { + NONE = 'none', JSON_FILE = 'jsonfile', MONGO_DB = 'mongodb', MYSQL = 'mysql', diff --git a/src/types/UIProtocol.ts b/src/types/UIProtocol.ts index 265f3760..30fd26c3 100644 --- a/src/types/UIProtocol.ts +++ b/src/types/UIProtocol.ts @@ -33,6 +33,7 @@ export enum ProcedureName { LIST_TEMPLATES = 'listTemplates', LIST_CHARGING_STATIONS = 'listChargingStations', ADD_CHARGING_STATIONS = 'addChargingStations', + PERFORMANCE_STATISTICS = 'performanceStatistics', START_CHARGING_STATION = 'startChargingStation', STOP_CHARGING_STATION = 'stopChargingStation', OPEN_CONNECTION = 'openConnection', diff --git a/src/utils/Configuration.ts b/src/utils/Configuration.ts index 06c2a5a3..19afc586 100644 --- a/src/utils/Configuration.ts +++ b/src/utils/Configuration.ts @@ -187,13 +187,19 @@ export class Configuration { } break case StorageType.JSON_FILE: - default: storageConfiguration = { enabled: false, type: StorageType.JSON_FILE, uri: getDefaultPerformanceStorageUri(StorageType.JSON_FILE) } break + case StorageType.NONE: + default: + storageConfiguration = { + enabled: true, + type: StorageType.NONE + } + break } if (hasOwnProp(Configuration.getConfigurationData(), ConfigurationSection.performanceStorage)) { storageConfiguration = { diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index ae27035f..cff63715 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -16,6 +16,7 @@ import { import { Constants } from './Constants.js' import { type EmptyObject, + type ProtocolResponse, type TimestampedData, WebSocketCloseEventStatusString } from '../types/index.js' @@ -343,8 +344,12 @@ export const secureRandom = (): number => { } export const JSONStringifyWithMapSupport = ( - object: Record | Array> | Map, - space?: number + object: + | Record + | Array> + | Map + | ProtocolResponse, + space?: string | number ): string => { return JSON.stringify( object, -- 2.34.1