feat: add performance statistics to UI protocol
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 5 Feb 2024 15:15:24 +0000 (16:15 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 5 Feb 2024 15:15:24 +0000 (16:15 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
18 files changed:
README.md
src/assets/config-template.json
src/assets/ui-protocol/Insomnia-CSSimulatorUIWSProtocolCollection.json
src/assets/ui-protocol/Insomnia_CSSimulatorUIProtocol.json
src/charging-station/Bootstrap.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/charging-station/ui-server/ui-services/AbstractUIService.ts
src/performance/storage/JsonFileStorage.ts
src/performance/storage/MikroOrmStorage.ts
src/performance/storage/MongoDBStorage.ts
src/performance/storage/None.ts [new file with mode: 0644]
src/performance/storage/Storage.ts
src/performance/storage/StorageFactory.ts
src/types/Storage.ts
src/types/UIProtocol.ts
src/utils/Configuration.ts
src/utils/Utils.ts

index 89ea7b6c10d3498dcd0d26bae966c40d6f6b2e5a..060d90bc53a822c6e8acdd65fab649cac639ed24 100644 (file)
--- 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                        |                                              | {<br />"enabled": true,<br />"file": "logs/combined.log",<br />"errorFile": "logs/error.log",<br />"statisticsInterval": 60,<br />"level": "info",<br />"console": false,<br />"format": "simple",<br />"rotate": true<br />} | {<br />enabled?: boolean;<br />file?: string;<br />errorFile?: string;<br />statisticsInterval?: number;<br />level?: string;<br />console?: boolean;<br />format?: string;<br />rotate?: boolean;<br />maxFiles?: string \| number;<br />maxSize?: string \| number;<br />}    | Log configuration section:<br />- _enabled_: enable logging<br />- _file_: log file relative path<br />- _errorFile_: error log file relative path<br />- _statisticsInterval_: seconds between charging stations statistics output in the logs<br />- _level_: emerg/alert/crit/error/warning/notice/info/debug [winston](https://github.com/winstonjs/winston) logging level</br >- _console_: output logs on the console<br />- _format_: [winston](https://github.com/winstonjs/winston) log format<br />- _rotate_: enable daily log files rotation<br />- _maxFiles_: maximum number of log files: https://github.com/winstonjs/winston-daily-rotate-file#options<br />- _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                     |                                              | {<br />"processType": "workerSet",<br />"startDelay": 500,<br />"elementStartDelay": 0,<br />"elementsPerWorker": 'auto',<br />"poolMinSize": 4,<br />"poolMaxSize": 16<br />}                                                | {<br />processType?: WorkerProcessType;<br />startDelay?: number;<br />elementStartDelay?: number;<br />elementsPerWorker?: number \| 'auto' \| 'all';<br />poolMinSize?: number;<br />poolMaxSize?: number;<br />resourceLimits?: ResourceLimits;<br />}                       | Worker configuration section:<br />- _processType_: worker threads process type (`workerSet`/`fixedPool`/`dynamicPool`)<br />- _startDelay_: milliseconds to wait at worker threads startup (only for `workerSet` worker threads process type)<br />- _elementStartDelay_: milliseconds to wait at charging station startup<br />- _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)<br />- _poolMinSize_: worker threads pool minimum number of threads</br >- _poolMaxSize_: worker threads pool maximum number of threads<br />- _resourceLimits_: worker threads [resource limits](https://nodejs.org/api/worker_threads.html#new-workerfilename-options) object option |
 | uiServer                   |                                              | {<br />"enabled": false,<br />"type": "ws",<br />"version": "1.1",<br />"options": {<br />"host": "localhost",<br />"port": 8080<br />}<br />}                                                                                | {<br />enabled?: boolean;<br />type?: ApplicationProtocol;<br />version?: ApplicationProtocolVersion;<br />options?: ServerOptions;<br />authentication?: {<br />enabled: boolean;<br />type: AuthenticationType;<br />username?: string;<br />password?: string;<br />}<br />} | UI server configuration section:<br />- _enabled_: enable UI server<br />- _type_: 'http' or 'ws'<br />- _version_: HTTP version '1.1' or '2.0'<br />- _options_: node.js net module [listen options](https://nodejs.org/api/net.html#serverlistenoptions-callback)<br />- _authentication_: authentication type configuration section                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
-| performanceStorage         |                                              | {<br />"enabled": false,<br />"type": "jsonfile",<br />"uri": "file:///performance/performanceRecords.json"<br />}                                                                                                            | {<br />enabled?: boolean;<br />type?: string;<br />uri?: string;<br />}                                                                                                                                                                                                         | Performance storage configuration section:<br />- _enabled_: enable performance storage<br />- _type_: 'jsonfile' or 'mongodb'<br />- _uri_: storage URI                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
+| performanceStorage         |                                              | {<br />"enabled": true,<br />"type": "none",<br />}                                                                                                                                                                           | {<br />enabled?: boolean;<br />type?: string;<br />uri?: string;<br />}                                                                                                                                                                                                         | Performance storage configuration section:<br />- _enabled_: enable performance storage<br />- _type_: 'jsonfile', 'mongodb' or 'none'<br />- _uri_: storage URI                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
 | stationTemplateUrls        |                                              | {}[]                                                                                                                                                                                                                          | {<br />file: string;<br />numberOfStations: number;<br />}[]                                                                                                                                                                                                                    | 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:  
index 5e18d942fd50ccdba139eec8db162e5e6811590c..641117eaa2bab27a1ad6b7b9c6d3a58ef0f3791c 100644 (file)
@@ -11,8 +11,8 @@
     "processType": "workerSet"
   },
   "performanceStorage": {
-    "enabled": false,
-    "type": "jsonfile"
+    "enabled": true,
+    "type": "none"
   },
   "uiServer": {
     "enabled": false,
index 07738fb1c86747c4359550b521ef4607ea40348f..f9661ed9f59b12ba45063ce3257b99f1e077bb30 100644 (file)
@@ -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": [
     {
       "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",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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]",
     {
       "_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"
     }
   ]
 }
index 413123a0d0c76746a18e261ff59a4b54e03e140f..43c9a3061f6650cdf45949187414ed8f17906833 100644 (file)
@@ -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": [
     {
       "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",
index f238d9515e1ba989d09176963323787b193c4cd7..2c1b0f43b3443f8f239888b7f4e507ad6eefd584 100644 (file)
@@ -120,6 +120,10 @@ export class Bootstrap extends EventEmitter {
     return this.chargingStationsByTemplate.get(templateName)?.lastIndex ?? 0
   }
 
+  public getPerformanceStatistics (): IterableIterator<Statistics> | undefined {
+    return this.storage?.getPerformanceStatistics()
+  }
+
   private get numberOfAddedChargingStations (): number {
     return [...this.chargingStationsByTemplate.values()].reduce(
       (accumulator, value) => accumulator + value.added,
index 7c102195413aed23e4a68e5ee28abf1657a9fcf8..27e8d7ba9051654d777f904b7e5ff16c884b0a47 100644 (file)
@@ -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}`
index 42162d16556c47cec0c0b7805f77fc36d3ac7f99..0dce52cace77c8cb27a67220f9bcb8788687fe55 100644 (file)
@@ -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(
index 9d044a09e2a0ce11a0514884fa2e58ca3e64d6c6..8617c7ab4051f375d1a7717185b2e753bf7c9976 100644 (file)
@@ -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<StorageConfiguration>(
+        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<ResponsePayload> {
     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
     }
   }
 }
index 31d18537f783067bebd396d40cf19fad34d69b0f..a69edd69ace56acf62fbd3992cb401e901fc5c3b 100644 (file)
@@ -14,8 +14,6 @@ import {
 } from '../../utils/index.js'
 
 export class JsonFileStorage extends Storage {
-  private static performanceRecords: Map<string, Statistics>
-
   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<string, Statistics>()
     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)
index a1cc7c8db99c38e0b217fd8a66cedef99a1944d5..e91693060aea213908d1df126e99aed81b930421 100644 (file)
@@ -19,6 +19,7 @@ export class MikroOrmStorage extends Storage {
 
   public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
     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<void> {
+    this.clearPerformanceStatistics()
     try {
       if (this.orm != null) {
         await this.orm.close()
index 48ce06d679d5131777542444bc46d87b3e40178b..b3d535e2906d33d6b13b1184e8b8458256f294a3 100644 (file)
@@ -20,6 +20,7 @@ export class MongoDBStorage extends Storage {
 
   public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
     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<void> {
+    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 (file)
index 0000000..6506d36
--- /dev/null
@@ -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()
+  }
+}
index d9539190cced18006c52d8845dd62ac5ec6b0b37..aee3ae9efbb5ca86b17a8a7ddf3fcd01b7b80684 100644 (file)
@@ -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<string, Statistics>()
 
   constructor (storageUri: string, logPrefix: string) {
     this.storageUri = new URL(storageUri)
@@ -53,6 +54,18 @@ export abstract class Storage {
     }
   }
 
+  public getPerformanceStatistics (): IterableIterator<Statistics> {
+    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<void>
   public abstract close (): void | Promise<void>
   public abstract storePerformanceStatistics (
index 8753bea1159d863f83b6a08cf2f0b102039d3219..9da6b54e98fa1d181b9a8c982e0b9d365535fa9f 100644 (file)
@@ -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}`)
index 4d4a4e4e621b25a98bce2759aab4dd6effd40a4e..232c9d41069d86b61a3f9ff661c20a1fc812f389 100644 (file)
@@ -1,4 +1,5 @@
 export enum StorageType {
+  NONE = 'none',
   JSON_FILE = 'jsonfile',
   MONGO_DB = 'mongodb',
   MYSQL = 'mysql',
index 265f376028f83692acb4be9e32497fb93efa7995..30fd26c370cc80f43591c908285ad84cf94737bc 100644 (file)
@@ -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',
index 06c2a5a361ad1c7e56e068485c3b0b3fac26b510..19afc5861111c7bd41e6ff2ba65e554e51deb35c 100644 (file)
@@ -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 = {
index ae27035ff079c028f30bee9884d5b2de5521c18a..cff63715719b07d254b045c133a9be2170e01291 100644 (file)
@@ -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<string, unknown> | Array<Record<string, unknown>> | Map<unknown, unknown>,
-  space?: number
+  object:
+  | Record<string, unknown>
+  | Array<Record<string, unknown>>
+  | Map<unknown, unknown>
+  | ProtocolResponse,
+  space?: string | number
 ): string => {
   return JSON.stringify(
     object,