@ti-engine/core 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # ti-engine changelog
2
2
 
3
+ ## Version 1.1.1
4
+
5
+ * feat(config): add new ENV variable `TI_AUDITING_LOG_DETAILS` that controls the `auditing.logDetails` setting
6
+ * feat(service provider): add a method that returns a list of the currently registered services
7
+ * feat(tester service): improved the structure, configuration, and inline docs of tester service
8
+ * fix(logger): fix the way Exception is logged when it's a part of the main logging data object
9
+ * docs(readme): expanded the general documentation in `README.md` with a new section and improved many of the older sections
10
+ * docs: improved some of the JSDoc definitions across the framework
11
+
3
12
  ## Version 1.1.0
4
13
 
5
14
  * feat(redis integration): implement better process for fetching and storing the remote Redis server settings and enabled features
package/README.md CHANGED
@@ -1,17 +1,21 @@
1
1
  # ti-engine core
2
+
2
3
  Flexible framework for the creation of microservices with [node.js](https://nodejs.org/).
3
4
 
4
5
  ## Introduction
5
- The **ti-engine** is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using **node.js**. The architectural concept of the framework is based on a standard _messaging system_ that allows for certain customization but also provides predictability and traceability of its behavior.
6
+
7
+ The **ti-engine** is an open source, free to use—both for personal and commercial projects—framework for the creation of microservice-based solutions using **node.js**. The architectural concept of the framework is based on a standard _messaging system_ that allows for certain customization but also provides predictability and traceability of its behavior.
6
8
 
7
9
  Being a messaging system, the **ti-engine** relies on a message broker for the actual exchange of messages between microservice instances. The default implementation of the framework uses [Redis](https://redis.io/) cache, however, you could create your own implementation using something like [Rabbit MQ](https://www.rabbitmq.com/). See the [Advanced topics](#advanced-topics) section of this documentation for guides on how to do this.
8
10
 
9
11
  Please be aware, that this framework is under active development and will expand in the near future. Also, this documentation is still in the process of being created and refined. Make sure to keep an eye on the changes in case you want to use it in the meantime.
10
12
 
11
13
  ## Why ti-engine?
14
+
12
15
  The framework is created based on a decade of professional experience with the utilized technologies and architectural approach. It's primary goal is to provide you with a lightweight and flexible solution that can help you build quickly a microservice ecosystem with any degree of size and complexity.
13
16
 
14
17
  This is what you gain by using **ti-engine** in your project:
18
+
15
19
  * Simplicity - begin productive work within minutes and get to codding you business logic
16
20
  * Flexibility - go as complex as you need to in your implementation
17
21
  * Reliability - message exchange between the services is constantly tracked across the entire ecosystem
@@ -22,7 +26,9 @@ This is what you gain by using **ti-engine** in your project:
22
26
  These are just some benefits **ti-engine** offers. Get to know it better to find out more ways in which it can help you improve productivity.
23
27
 
24
28
  ## Prerequisites & installation
29
+
25
30
  In order to run the basic ti-engine framework you will need a couple of things:
31
+
26
32
  * A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **14.17.0**
27
33
  * A local or remote [Redis cache installation](https://redis.io/download) with a minimum version of **5.0.14**
28
34
 
@@ -31,69 +37,75 @@ If you are working under Windows 10+ OS and you need to install Redis, take a lo
31
37
  To get the framework itself, use the command `npm install @ti-engine/core`. And to include it directly in your package.json dependencies execute `npm install @ti-engine/core --save-prod`.
32
38
 
33
39
  ## Getting started
40
+
34
41
  To start using the **ti-engine**, you will have to make sure that all prerequisites are available and operational. However, before we get to the fun part you need to also consider a couple of very important things while working with this framework:
42
+
35
43
  1. The runtime configuration of the framework can be customized using ENV variables. These can be provided to node.js in all the standard ways, but there is also an option to include an `.env` file.
36
44
  2. It loads your framework-related custom scripts and files dynamically, but it always assumes their provided paths are relative to the _current working directory_ (i.e. it uses `process.cwd()`). Be mindful of that whenever you declare file paths in the various settings.
37
45
 
38
46
  Once you have everything else ready, you should download the **ti-engine** tester module with the command `npm install @ti-engine/tester`. The tester module packages an example microservice that shows the basic approach for using the framework. To make sure everything is working properly, you should try and start the tester service:
47
+
39
48
  1. Open a command prompt and navigate to the directory of the tester module; it should be something like that:
40
- `[path to your project]/node_modules/@ti-engine/tester`
41
- 2. Execute the following command `node ../core/bin/start-instance.js`
49
+ `[path to your project]/node_modules/@ti-engine/tester`
50
+ 2. Execute the following command `node ../core/bin/start-instance.js`. Keep in mind that the working directory for the node process has to be the one specified in point 1. Otherwise, you'll get errors that certain files cannot be found and loaded.
42
51
  3. If everything was done properly, you should see the following output:
52
+
43
53
  ```text
44
54
  [timestamp]: [instance-id] - notice - Starting new instance of type 'tester-service' with instance ID '[instance-id]'.
45
55
  [timestamp]: [instance-id] - info - Starting service registration process. There is NO default service handler provided.
46
56
  [timestamp]: [instance-id] - info - Registration of defined services completed with 2 successful out of 2 total.
47
57
  [timestamp]: [instance-id] - notice - Instance '[instance-id]' started successfully.
58
+ » {"nodeVersion":[node-version]}
48
59
  [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-responses-out' and is ready to be used.
49
- [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'system' and is ready to be used.
60
+ [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'system-cache' and is ready to be used.
50
61
  [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-requests-in' and is ready to be used.
51
62
  [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-requests-out' and is ready to be used.
52
63
  [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-responses-in' and is ready to be used.
53
64
  [timestamp]: [instance-id] - notice - Execution of service1 result:
54
- { exception: undefined, isSuccessful: true, payload: { s1Timestamp: [timestamp] } }
65
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp]}}
55
66
  [timestamp]: [instance-id] - notice - Execution of service2 result:
56
- { exception: undefined, isSuccessful: true, payload: { s1Timestamp: [timestamp], s2TimestampStart: [timestamp], s2TimestampEnd: [timestamp] } }
67
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
57
68
  ```
69
+
58
70
  Now let's analyse that output. For the sake of completeness, the `[timestamp]` and `[instance-id]` are placeholders of the actual values you'll see there. The timestamps are in UTC and show a date followed by time.
59
71
 
60
72
  At the start of the output log you can see a NOTICE that tells you a couple of important things:
73
+
61
74
  * The instance name - in this case `tester-service`. In the terminology of the framework, this is also known as a _service domain name_.
62
75
  * The _instance identificator_. It is an uuid string with a `ti-` prefix, that is generated by the framework at process start. It can and will be used to trace the messages during their movement through the microservice ecosystem. But more on that later.
63
76
 
64
77
  Following that come a couple of INFO lines that inform you about the microservice interface state. The framework starts with the process of registration of _business services_ within the service domain of the microservice `tester-service` and successfully adds 2 such services. The necessary information for this is read from a JSON config file included in the package. We'll get into more details on what this all means in the section [Creating a microservice](#creating-a-microservice).
65
78
 
66
79
  Once the initialization sequence has completed the framework informs you that the microservice instance has started successfully. If the framework encountered an error during initialization instead, you would see something like this:
80
+
67
81
  ```text
68
82
  [timestamp]: [instance-id] - notice - Starting new instance of type 'tester-service' with instance ID '[instance-id]'.
69
83
  [timestamp]: [instance-id] - alert - Error detected in the instance startup script!
70
84
  ```
85
+
71
86
  The following 5 lines inform you about the successful connection to Redis. The default configuration assumes that your Redis is running on localhost and uses the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [Using the framework](#using-the-framework).
72
87
 
73
88
  Finally, you should see a couple of execution statements with their results in JSON format.
74
89
 
75
90
  You can now kill the node process which should show you the following two lines:
91
+
76
92
  ```text
77
93
  [timestamp]: [instance-id] - notice - SIGINT event detected in main instance process.
78
94
  [timestamp]: [instance-id] - notice - Instance '[instance-id]' shut down successfully.
79
95
  ```
96
+
80
97
  The framework will always try to capture the shut-down event and log it. This should work even in container environment, but it might depend on your setup whether the last two entries will reach the logging system or not.
81
98
 
82
- The tester module gets its starting configuration from an `.env` file included in the package. If you open it, this is what you'll see:
83
- ```text
84
- TI_INSTANCE_CLASS=tester-service.js
85
- TI_INSTANCE_CONFIG=tester-service.json
86
- TI_INSTANCE_NAME=tester-service
87
- TI_AUDITING_LOG_MIN_LEVEL=200
88
- ```
89
- The first variable `TI_INSTANCE_CLASS` is mandatory for every microservice you create with the **ti-engine**. It needs to specify the path to the module that is your microservice. Remember, that this path has to be relative to the working directory in which you plan to execute the `node` command. This is especially important when you're configuring your microservices to work in containers. You can find the full list of available ENV variables and what they do below.
99
+ The tester module gets its starting configuration from an `.env` file included in the package. You can find more information about it later in the [Creating a microservice](#creating-a-microservice) section.
90
100
 
91
101
  Before moving on, also take a good look at the file `bin/start-instance.js`. It should give you an idea on how to the process of starting and stopping a microservice operates. In most cases this file should be sufficient as a starting script for your **ti-engine** based microservice applications. You can, of course, create your own starting script, but then you'll have to consider all necessary steps to properly handle the microservice instance.
92
102
 
93
103
  ## Architecture
94
- The architectural approach for the **ti-engine** is done in _tiers_ with lower tiers being unaware of the tiers above them. The framework prefers a high level of abstraction in all its tiers and provides many options for customization and extension. While the language is JavaScript, the structuring of the framework follows the OOP principles, and you will find a lot of abstract classes and methods that require you to implement them. These are always marked with the `@abstract` annotation but if you happen to miss one, the framework will raise an exception when you try to use it in your solution.
104
+
105
+ The architectural approach for the **ti-engine** is done in _tiers_ with lower tiers being unaware of the tiers above them. The framework prefers a high level of abstraction in all its tiers and provides many options for customization and extension. While the language is JavaScript, the structuring of the framework follows the OOP principles, and you will find a lot of abstract classes and methods that require you to implement them. These are always marked with the `@abstract` annotation but if you happen to miss one, the framework will raise an `E_GEN_ABSTRACT_METHOD_CALL` exception when you try to use it in your solution.
95
106
 
96
107
  There are three general tiers in the **ti-engine**:
108
+
97
109
  1. Message exchange
98
110
  2. Service domains
99
111
  3. Solution implementation
@@ -101,7 +113,8 @@ There are three general tiers in the **ti-engine**:
101
113
  See the following sections for more information on each of them.
102
114
 
103
115
  ### Tier 1 - Message exchange
104
- This is the lowest framework tier, unless we count the actual data objects processed by the framework. As you already know, the foundational **ti-engine** concept is that of a messaging system. Therefore, the first tier provides an abstraction over a chosen message broker (Redis by default). That abstraction makes it easy to switch between message brokers whenever you want to without having to change anything above tier 1. It also provides several added bonuses that can accelerate your work - message encryption, message tracing, message observers, and others. More details about each of these features will be covered in section [Using the framework](#using-the-framework).
116
+
117
+ This is the lowest framework tier, unless we count the actual data objects processed by the framework. As you already know, the foundational **ti-engine** concept is that of a messaging system. Therefore, the first tier provides an abstraction over a chosen message broker (Redis by default). That abstraction makes it easy to switch between message brokers whenever you want to without having to change anything above tier 1. It also provides several added bonuses that can accelerate your work—message encryption, message tracing, message observers, and others. More details about each of these features will be covered in section [Using the framework](#using-the-framework).
105
118
 
106
119
  Another important aspect for you to remember is that the message exchange is entirely _asynchronous_. This helps reduce system load and optimizes the usage of the available resources. Even so each node.js process can handle a limited amount of load. Therefore, you should plan for running multiple identical senders and receives in order to scale your solution. But more on that later.
107
120
 
@@ -113,26 +126,31 @@ It shows the standard flow of a message exchange between one sender and _n_ iden
113
126
 
114
127
  After the processing is done the message payload is modified and the receiver sends the message back to the original sender using the same mechanism. It again splits the message into an envelope and a payload, stores the payload in the storage and enqueues the envelope in the sender response (source) queue. The sender will then assemble the message back and process the contained results. This process is depicted by the red flow lines.
115
128
 
116
- In this scenario the framework utilizes _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. Other message brokers might utilize a slightly different approach, but they should still adhere to the same logical flow.
129
+ In this scenario the framework utilizes _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. The splitting between envelope and payload is done in order to avoid unnecessary transportation of potentially large volumes of operational data between the microservices. Other message brokers might utilize a slightly different approach, but they should still adhere to the same logical flow.
117
130
 
118
131
  The modules associated with this tier are all located in the `components/exchange/` folder. This is a short list of some terminology used here and in the JDoc inside the sourcecode itself:
132
+
119
133
  * Message - this is the actual data object processed by the framework. It consists of two parts: an envelope containing service information and a payload containing the actual data to be processed.
120
134
  * Message sender - a specialized connector that is responsible for sending a message on its way to its destination. It does not handle the actual dispatch and delivery.
121
135
  * Message receiver - a specialized connector that is responsible for receiving messages at predefined destination.
122
136
  * Message exchange - this is the actual message processing engine. It handles sending and receiving messages via preconfigured message senders and message receivers.
123
- * Message observer - a custom event listener that can be used to react on message `send` and `received` events.
137
+ * Message observer - a custom event listener that can be used to react on message `sent` and `received` events.
124
138
 
125
139
  ### Tier 2 - Service domains
140
+
126
141
  This tier focuses on hosting and executing the _business logic_ of your application. It's comprised of _business services_ that process input data and return the result of the processing as output data. The business services are grouped in _service domains_, which are in turn hosted inside stateless _microservices_ also named _service instances_. There are two types of service instances in **ti-engine**:
127
- * Service consumers - these are service instances, that can call business services in any available service domain.
142
+
143
+ * Service consumers - these are service instances, that can call business services in any connected and available service domain.
128
144
  * Service providers - these are service instances, that host and run a set of business services in a particular service domain. Every service provider is also a service consumer.
129
145
 
130
- The various service instances in a solution represent a network of interconnected service domains that contain the business logic of your application. All business services exchange data via _service calls_ using abstract _service addresses_. These service calls are transported from one address in the network to another via the underlying message exchange tier. This, however, is completely transparent to the service instances. In essence, tier 2 does not care about the actual data transportation method or protocol. You could in fact change completely the tier 1 approach without having to modify anything in your business logic and business flow.
146
+ The various service instances in a solution represent a network of interconnected service domains that contain the business logic of your application. All business services exchange data via _service calls_ using abstract _service addresses_. These service calls are transported from one address in the microservice network to another via the underlying message exchange tier. This, however, is completely transparent to the service instances. In essence, tier 2 does not care about the actual data transportation method or protocol. You could in fact change completely the tier 1 approach without having to modify anything in your business logic and business flow.
131
147
 
132
- This tier is the place to utilize any databases, file storages, integrations with other applications, scheduling jobs, and so on. In general, it should focus on executing any granular tasks that are essential to backbone operation of your application. The business logic here should remain stateless and any user context should be provided at runtime to each invoked business service. We'll see more concrete examples for that later in section [Using the framework](#using-the-framework).
148
+ This tier is the place to utilize any databases, file storages, integrations with other applications, scheduling jobs, and so on. In general, it should focus on executing any granular tasks that are essential to the backbone operation of your application. The business logic here should remain **stateless** and any user context should be provided at runtime to each invoked business service. We'll see more concrete examples for that later in section [Using the framework](#using-the-framework).
133
149
 
134
150
  ### Tier 3 - Solution implementation
135
- This tier comprises the actual implementation of your application. Its structure and behavior depends entirely on your vision and business goals. There are still a couple of points that remain constant:
151
+
152
+ This tier comprises the actual implementation of your application. Its structure and behavior depends entirely on your vision and business goals. There are still a couple of points that remain constant while using **ti-engine**:
153
+
136
154
  * It needs to utilize the business logic defined in tier 2 by calling the business services
137
155
  * It needs to take care of any type of stateful behavior like user sessions or transactions
138
156
  * It needs to act as the primary interface between users and your application thus handling access management and user interactions
@@ -140,10 +158,184 @@ This tier comprises the actual implementation of your application. Its structure
140
158
  Depending on the type of software you are building, tier 3 can be an API Gateway, a Web application, backend for a Mobile application, or anything like that.
141
159
 
142
160
  ## Creating a microservice
143
- Under development...
161
+
162
+ Now let's walk through the process of creating a microservice with **ti-engine**. We'll start with analyzing the contents of the tester module. Then we'll proceed with creating a new microservice that can call one of the business services in the default `ti-tester-service`.
163
+
164
+ ### The ti-tester microservice
165
+
166
+ If you managed to execute the initial framework test as explained in the [Getting started](#getting-started) section, you should already be familiar with the default tester microservice. Here we'll dissect its contents even further.
167
+
168
+ Let's take a look at the files and file structure first (only relevant items are shown):
169
+
170
+ ```text
171
+ bin
172
+ ↳ services
173
+ ↳ v1
174
+ ↳ service1.js
175
+ ↳ service2.js
176
+ ↳ tester-service.js
177
+ ↳ tester-service.json
178
+ .env
179
+ package.json
180
+ ```
181
+
182
+ You don't have to follow the exact same folder structure as most of the paths can be defined via ENV parameters and in the configuration file. However, having a good clean structure helps when organizing your work in more complex projects.
183
+
184
+ In the default tester microservice all application files are located inside the `bin` folder. Outside you have only the `package.json` and the `.env` files which can be considered more of a configuration for the node process rather than part of the application itself. Nevertheless, let's start with them:
185
+
186
+ #### package.json contents
187
+
188
+ ```json
189
+ {
190
+ "name": "@ti-engine/tester",
191
+ "version": "...",
192
+ "description": "...",
193
+ "author": "...",
194
+ "license": "ISC",
195
+ "dependencies": {
196
+ "@ti-engine/core": "latest"
197
+ },
198
+ "engines": {
199
+ "node": ">=14.17.0"
200
+ }
201
+ }
202
+ ```
203
+
204
+ Apart from the standard information properties there are only two important entries here: `"@ti-engine/core": "latest"` and `"node": ">=14.17.0"`. The dependency on the core of the framework is set to `latest`, but as with any other npm library you should set this to a specific version when releasing on production. The minimum node version should also reflect the minimum requirements of your application and can be adjusted accordingly, but it should not go below the minimum version required by **ti-engine**.
205
+
206
+ #### .env contents
207
+
208
+ ```text
209
+ TI_INSTANCE_CLASS=bin/tester-service.js
210
+ TI_INSTANCE_CONFIG=bin/tester-service.json
211
+ TI_INSTANCE_NAME=tester-service
212
+ TI_AUDITING_LOG_MIN_LEVEL=200
213
+ ```
214
+
215
+ The ENV initialization file provides the minimal settings for the proper tester microservice operation. The first three are usually _mandatory_ for every microservice you create while the last is provided for the needs of the tester demonstration. Let's review them and see what they do:
216
+
217
+ * `TI_INSTANCE_CLASS` specifies the relative path to the implementation of the `ServiceInstance` framework class—in this case a `ServiceProvider`. As stated above, the path is relative to the working directory of the `node` process. This variable is mandatory for every microservice you create with the **ti-engine**. If it is not provided the microservice won't be able to start at all, and you will get an exception.
218
+ * `TI_INSTANCE_CONFIG` specifies the relative path to the configuration data for the microservice. We'll delve into the specific settings below. Technically, you can omit this variable and the microservice will still start successfully with an empty configuration. There are very few cases, however, where this would be applicable.
219
+ * `TI_INSTANCE_NAME` is the _service domain_ name provided for the microservice. It has to be _unique_ in the context of the microservice ecosystem. If not provided, the framework will attempt to extract this information from the name of the implementation file. That is not a recommended approach though as it might cause hard to identify errors later.
220
+ * `TI_AUDITING_LOG_MIN_LEVEL` specifies the minimum log level that should be sent to the log output stream. With a setting of `200` (corresponding to INFO) we filter out all `DEFAULT(0)` and `DEBUG(100)` entries as we don't need them for the purposes of the tester microservice.
221
+
222
+ You can find the full list of available ENV variables and what they do below in [Using the framework](#using-the-framework) section.
223
+
224
+ #### Application specific files
225
+
226
+ Now let's look inside the `bin` folder. The two files there are the ones specified in the `.env` file. The `tester-service.js` contains the implementation of the `ServiceProvider` class. It has just three methods that contain its behavior:
227
+
228
+ * Method `onStart` overrides the base one from the parent class and is invoked automatically by the framework once initialization of the microservice is complete. In this case the method invokes the execution of the test sequence just once and then the microservice remains dormant but active.
229
+ * Method `verifyAccess` also overrides the base method and shows a very basic example of how to implement user access verification on business service level. Each time a service in the `ti-tester-service` is called, the framework will trigger this method and will only allow processing if there is a non-undefined value inside the `authToken` variable.
230
+ * Method `#executeTests` is a custom private method that contains the test sequence itself. It is called by the `onStart` method just once per microservice start. Inside you can see two examples of calling a business service—in both cases the tester microservice is calling itself. In more practical situation, however, these calls would be directed towards other service domains.
231
+
232
+ The file `tester-service.json` contains framework configuration for the tester microservice. It will be automatically loaded inside the `ServiceInstance` class during initialization and will already be available inside the `onStart` method for usage. In this case the configuration is related to the two business services that will be provided by the microservice. More on this topic will be covered in section [Using the framework](#using-the-framework). For now just pay attention to the `serviceFile` parameter and that it once again provides a relative path to the actual file containing the business logic.
233
+
234
+ The final two files are located in `bin/services/v1/` folder. They contain the definitions and business logic of the two business services that will be loaded at initialization time and provided by the tester microservice. In this case `service1.js` contains a very simple service that returns the current timestamp. The `service2.js` file contains a slightly more complex example of a service calling another service (in this case `service1`) before also returning two timestamps taken at the beginning and end of execution. Pay attention to the way the methods inside are declared and exported as this is the proper way to do this while using the **ti-engine** framework. Once again, we'll delve into the details and specifics of creating business services in section [Using the framework](#using-the-framework).
235
+
236
+ ### Creating your own microservice
237
+
238
+ Now that we've seen the structure of the tester microservice, let's create a new one and make it call `service2`. Let's use similar file structure as for the tester. Create a folder `my-service` and in it create a `package.json` file. Make sure to include a dependency to `"@ti-engine/core": "latest"` in it. After that create a `.env` file and add the following entries in it:
239
+
240
+ ```text
241
+ TI_INSTANCE_CLASS=bin/my-service.js
242
+ TI_INSTANCE_CONFIG=bin/my-service.json
243
+ TI_INSTANCE_NAME=my-service
244
+ TI_AUDITING_LOG_MIN_LEVEL=200
245
+ ```
246
+
247
+ Create the `my-service.json` file in a `my-service/bin/` folder. For now enter an empty JSON object `{}` inside and leave at that. Next create the `my-service.js` file in the same location and let's start entering some code in it. Since this will be a microservice that only uses other's services, we'll inherit the `ServiceConsumer` class instead:
248
+
249
+ ```js
250
+ const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
251
+
252
+ class MyService extends ServiceConsumer {}
253
+
254
+ module.exports = MyService;
255
+ ```
256
+
257
+ Don't forget to also export your new class at the end, otherwise the framework won't be able to initialize it.
258
+
259
+ To make use of all the inherited features of the `ServiceConsumer` class we have to add a `constructor` that invokes the base one in the parent class:
260
+
261
+ ```js
262
+ const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
263
+
264
+ class MyService extends ServiceConsumer {
265
+ constructor( serviceDomainName, serviceConfig ) {
266
+ super( serviceDomainName, serviceConfig );
267
+ }
268
+ }
269
+
270
+ module.exports = MyService;
271
+ ```
272
+
273
+ And now let's add a service call that is executed at microservice start after `500` milliseconds timeout:
274
+
275
+ ```js
276
+ const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
277
+ const logger = require( "@ti-engine/core/logger" );
278
+ const exceptions = require( "@ti-engine/core/exceptions" );
279
+ const { setTimeout: setTimeoutPromise } = require( "node:timers/promises" );
280
+
281
+ class MyService extends ServiceConsumer {
282
+ constructor( serviceDomainName, serviceConfig ) {
283
+ super( serviceDomainName, serviceConfig );
284
+ }
285
+
286
+ onStart() {
287
+ return new Promise( ( resolve, reject ) => {
288
+ super.onStart().then( () => {
289
+ return setTimeoutPromise( 500 );
290
+ } ).then( () => {
291
+ return this.callService( {
292
+ serviceAlias: "service2",
293
+ serviceDomainName: "ti-tester-service"
294
+ }, {}, {
295
+ authToken: "auth"
296
+ } );
297
+ } ).then( ( result ) => {
298
+ logger.log( "Execution of service2 result:", logger.logSeverity.NOTICE, result );
299
+ resolve();
300
+ } ).catch( ( error ) => {
301
+ reject( exceptions.raise( error ) );
302
+ } );
303
+ } );
304
+ }
305
+ }
306
+
307
+ module.exports = MyService;
308
+ ```
309
+
310
+ Now let's start the new microservice with `node .\node_modules\@ti-engine\core\bin\start-instance.js` command. Remember, you need to execute this inside the `my-service` folder you created for this exercise. If everything was configured correctly you should get the following output:
311
+
312
+ ```text
313
+ [timestamp]: [instance-id] - notice - Starting new instance of type 'my-service' with instance ID '[instance-id]'.
314
+ [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-responses-in' and is ready to be used.
315
+ [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-requests-out' and is ready to be used.
316
+ [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'system-cache' and is ready to be used.
317
+ ```
318
+
319
+ If you haven't started anything else, this is all you should see at this point.
320
+
321
+ Now without exiting this node process let's start the original tester microservice as well. Once it initializes and does its work take a look at the output logs of the `my-service` process:
322
+
323
+ ```text
324
+ ...
325
+ [timestamp]: [instance-id] - notice - Execution of service2 result:
326
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
327
+ [timestamp]: [instance-id] - notice - Instance [instance-id] started successfully.
328
+ » {"nodeVersion":[node-version]}
329
+ ```
330
+
331
+ This means the service call processing was successful and result was returned to `my-service`. Because we made the receiving of the result blocking and part of the initialization sequence, the new microservice did not report successful startup until it received that response from `ti-tester-service`.
332
+
333
+ And with this we are ready. The new microservice is now operational. You can continue to tweak and play with it in order to understand better how it all works. For more details on the **ti-engine** inner working please see the following sections.
144
334
 
145
335
  ## Using the framework
336
+
146
337
  Under development...
147
338
 
148
339
  ## Advanced topics
149
- Under development...
340
+
341
+ Under development...
@@ -20,7 +20,7 @@ const messageDispatcher = require( "#message-dispatcher" );
20
20
 
21
21
  /**
22
22
  * @typedef {Object} ServiceExecContext
23
- * @property {string} authToken A valid authentication token that initialized the service call.
23
+ * @property {string|undefined} authToken A valid authentication token that initialized the service call (if applicable).
24
24
  * @property {ServiceCallPredecessor|undefined} previousServiceCall The previous service call in the execution chain (if such exists).
25
25
  */
26
26
 
@@ -36,7 +36,7 @@ const messageDispatcher = require( "#message-dispatcher" );
36
36
  * @property {string} authToken A valid authentication token that initialized the service call.
37
37
  * @property {number} createdOn A unix timestamp taken at creation time of the service call.
38
38
  * @property {number} executionTime The total execution time of this service call in milliseconds.
39
- * @property {Object|undefined} exception If there was exception during the service call processing, it will be set here. Otherwise it will be 'undefined'.
39
+ * @property {Object|undefined} exception If there was exception during the service call processing, it will be set here. Otherwise, it will be 'undefined'.
40
40
  * @property {number|undefined} finishedOn A unix timestamp taken at finish time of the service call.
41
41
  * @property {boolean} isCompleted Flag to indicate if this service call has been completed.
42
42
  * @property {boolean|undefined} isSuccessful A flag indicating if this service call can be considered successful or not. Will be 'undefined' until the service call is processed.
@@ -45,9 +45,9 @@ const messageDispatcher = require( "#message-dispatcher" );
45
45
 
46
46
  /**
47
47
  * @typedef {Object} ServiceCallResult
48
- * @property {Object|undefined} exception If there was exception during the service call processing, it will be set here. Otherwise it will be 'undefined'.
48
+ * @property {Object|undefined} exception If there was exception during the service call processing, it will be set here. Otherwise, it will be 'undefined'.
49
49
  * @property {boolean} isSuccessful A flag indicating if this service call can be considered successful or not.
50
- * @property {Object|string|undefined} payload The payload containing the results from the service call processing. If string, it is ID of the payload in the memory cache instead.
50
+ * @property {Object|string|undefined} payload The payload containing the results from the service call processing. If string it is ID of the payload in the memory cache instead.
51
51
  */
52
52
 
53
53
  /**
@@ -78,7 +78,7 @@ class ServiceCaller extends MessageObserver {
78
78
  /**
79
79
  * Used to call a service in the service ecosystem asynchronously.
80
80
  * <br/>
81
- * NOTE: This method will timeout after specific preconfigured time, in which case it will resolve with {@link E_COM_SERVICE_EXEC_TIMEOUT} error.
81
+ * NOTE: This method will time out after specific preconfigured time, in which case it will resolve with {@link E_COM_SERVICE_EXEC_TIMEOUT} error.
82
82
  *
83
83
  * @method
84
84
  * @param {ServiceAddress} serviceAddress The service address has to define a valid service domain name, service alias, and optionally a service version.
@@ -19,6 +19,14 @@ const messageDispatcher = require( "#message-dispatcher" );
19
19
  * @property {number} [serviceVersion] Service version.
20
20
  */
21
21
 
22
+ /**
23
+ * @typedef {Object.<string, ServiceInterfaceVersion>} ServiceInterface
24
+ */
25
+
26
+ /**
27
+ * @typedef {Object.<number, ServiceHandlerMethod>} ServiceInterfaceVersion
28
+ */
29
+
22
30
  /**
23
31
  * @callback VerifyAccessMethod
24
32
  * @param {string} authToken
@@ -43,6 +51,7 @@ const messageDispatcher = require( "#message-dispatcher" );
43
51
  */
44
52
  class ServiceExecutor extends MessageObserver {
45
53
 
54
+ /** @type ServiceInterface */
46
55
  #serviceInterface = {};
47
56
  /** @type VerifyAccessMethod */
48
57
  #verifyAccess;
@@ -62,6 +71,15 @@ class ServiceExecutor extends MessageObserver {
62
71
 
63
72
  /* Public interface */
64
73
 
74
+ /**
75
+ * Property returning the current service interface.
76
+ *
77
+ * @property
78
+ * @returns {ServiceInterface}
79
+ * @public
80
+ */
81
+ get serviceInterface() { return this.#serviceInterface; }
82
+
65
83
  /**
66
84
  *
67
85
  *
@@ -117,7 +135,7 @@ class ServiceExecutor extends MessageObserver {
117
135
  }
118
136
 
119
137
  /**
120
- * Used to setup the method for service access verification.
138
+ * Used to set up the method for service access verification.
121
139
  *
122
140
  * @method
123
141
  * @param {VerifyAccessMethod} verifyAccess
@@ -12,10 +12,15 @@ const exceptions = require( "#exceptions" );
12
12
  const cache = require( "#cache" );
13
13
  const messageDispatcher = require( "#message-dispatcher" );
14
14
 
15
+ /**
16
+ * @typedef {Object} ServiceConfiguration
17
+ * @property {ServiceDefinition[]} services A list of service definitions to be registered with the {@link ServiceProvider}.
18
+ */
19
+
15
20
  /**
16
21
  * Abstract class used to define a Service Instance behavior.
17
22
  * <br/>
18
- * NOTE: Inherit this to create an a module that can be started as a microservice instance.
23
+ * NOTE: Inherit this to create a module that can be started as a microservice instance.
19
24
  * <br/>
20
25
  * NOTE: This class does not
21
26
  *
@@ -27,6 +32,7 @@ class ServiceInstance {
27
32
 
28
33
  static #instanceID;
29
34
  static #serviceDomainName;
35
+ /** @type ServiceConfiguration */
30
36
  #serviceConfig;
31
37
  #serviceHealthCheck;
32
38
  #reportHealthyJob;
@@ -44,7 +50,7 @@ class ServiceInstance {
44
50
 
45
51
  ServiceInstance.#instanceID = process.env.TI_INSTANCE_ID || tools.getUUID();
46
52
  ServiceInstance.#serviceDomainName = serviceDomainName;
47
- this.#serviceConfig = ( _.isObjectLike( serviceConfig ) ) ? serviceConfig : {};
53
+ this.#serviceConfig = ( _.isObjectLike( serviceConfig ) ) ? serviceConfig : { services: [] };
48
54
  }
49
55
 
50
56
  /* Public interface */
@@ -80,7 +86,7 @@ class ServiceInstance {
80
86
  * Property returning the service configuration JSON.
81
87
  *
82
88
  * @property
83
- * @returns {Object}
89
+ * @returns {ServiceConfiguration}
84
90
  * @public
85
91
  */
86
92
  get serviceConfig() { return this.#serviceConfig; }
@@ -13,7 +13,7 @@ const messageDispatcher = require( "#message-dispatcher" );
13
13
  /**
14
14
  * Abstract class used to define a Service Provider behavior.
15
15
  * <br/>
16
- * NOTE: Inherit this to create an a module that can be started as a microservice provider instance.
16
+ * NOTE: Inherit this to create a module that can be started as a microservice provider instance.
17
17
  * <br/>
18
18
  * NOTE: A service provider is a microservice that offers an API of named business services that can be invoked by other
19
19
  * microservices using {@link ServiceCall} objects. The provider will take care of the actual execution of that service and
@@ -28,6 +28,7 @@ const messageDispatcher = require( "#message-dispatcher" );
28
28
  */
29
29
  class ServiceProvider extends ServiceConsumer {
30
30
 
31
+ /** @type ServiceExecutor */
31
32
  #serviceExecutor;
32
33
 
33
34
  /**
@@ -104,7 +105,7 @@ class ServiceProvider extends ServiceConsumer {
104
105
  /**
105
106
  * Used to verify whether the service caller has authorization to access the service.
106
107
  * <br/>
107
- * NOTE: Override this to implement authorization check. By default this method simply returns.
108
+ * NOTE: Override this to implement authorization check. By default, this method simply returns.
108
109
  *
109
110
  * @method
110
111
  * @param {string} authToken
@@ -124,7 +125,7 @@ class ServiceProvider extends ServiceConsumer {
124
125
  * keep in mind that your first param must always be the 'serviceDefinition' and the second one will be the general 'serviceParams' object.
125
126
  * <br/>
126
127
  * NOTE: Additionally, if you intend to call another service inside the service handler, then you have to use normal function for the handler and not
127
- * an arrow function! Arrow functions cannot bind the scope of the parent class to themselves and you won't have access to it and its methods.
128
+ * an arrow function! Arrow functions cannot bind the scope of the parent class to themselves, and you won't have access to it and its methods.
128
129
  *
129
130
  * @method
130
131
  * @param {ServiceDefinition} serviceDefinition Full service definition object.
@@ -180,7 +181,7 @@ class ServiceProvider extends ServiceConsumer {
180
181
  let promises = [];
181
182
  _.forEach( serviceDefinitions, ( serviceDefinition ) => {
182
183
  // NOTE: we are not going to interrupt the service interface loading if one of the services fails to load or is not found!
183
- // If this happens, a corresponding log entry will be created but the loading process will continue. Therefore the following
184
+ // If this happens, a corresponding log entry will be created but the loading process will continue. Therefore, the following
184
185
  // promise will always resolve (unless a programming error occurs in it of course).
185
186
  let registrationPromise = ( serviceDefinition, defaultServiceHandler ) => {
186
187
  return new Promise( ( resolve, reject ) => {
@@ -211,6 +212,17 @@ class ServiceProvider extends ServiceConsumer {
211
212
  } );
212
213
  }
213
214
 
215
+ /**
216
+ * Used to get an ordered list of all currently registered services. This does not include the service versions.
217
+ *
218
+ * @method
219
+ * @returns {string[]}
220
+ * @public
221
+ */
222
+ getRegisteredServices() {
223
+ return _.sortBy( _.keys( this.#serviceExecutor.serviceInterface ) );
224
+ }
225
+
214
226
  }
215
227
 
216
228
  module.exports = ServiceProvider;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ti-engine/core",
3
- "version": "1.1.0",
4
- "description": "The ti-engine is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using node.js.",
3
+ "version": "1.1.1",
4
+ "description": "The ti-engine is an open source, free to useboth for personal and commercial projectsframework for the creation of microservice-based solutions using node.js.",
5
5
  "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
6
  "license": "ISC",
7
7
  "exports": {
package/utils/config.js CHANGED
@@ -21,6 +21,7 @@ const tools = require( "#tools" );
21
21
  * @property {EnvironmentVariable} env.TI_INSTANCE_ID
22
22
  * @property {EnvironmentVariable} env.TI_INSTANCE_NAME
23
23
  * @property {EnvironmentVariable} env.TI_AUDITING_LOG_CONSOLE_ENABLED
24
+ * @property {EnvironmentVariable} env.TI_AUDITING_LOG_DETAILS
24
25
  * @property {EnvironmentVariable} env.TI_AUDITING_LOG_MIN_LEVEL
25
26
  * @property {EnvironmentVariable} env.TI_AUDITING_LOG_USES_JSON
26
27
  * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_AUTH_KEY
@@ -134,8 +135,9 @@ const settings = require( "#settings" );
134
135
 
135
136
  // override remaining settings with ENV variables (if provided):
136
137
  if ( settings.auditing ) {
137
- settings.auditing.logMinLevel = ( process.env.TI_AUDITING_LOG_MIN_LEVEL !== undefined ) ? process.env.TI_AUDITING_LOG_MIN_LEVEL : settings.auditing.logMinLevel;
138
138
  settings.auditing.logConsoleEnabled = ( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_CONSOLE_ENABLED ) : settings.auditing.logConsoleEnabled;
139
+ settings.auditing.logDetails = ( process.env.TI_AUDITING_LOG_DETAILS !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_DETAILS ) : settings.auditing.logDetails;
140
+ settings.auditing.logMinLevel = ( process.env.TI_AUDITING_LOG_MIN_LEVEL !== undefined ) ? process.env.TI_AUDITING_LOG_MIN_LEVEL : settings.auditing.logMinLevel;
139
141
  settings.auditing.logUsesJSON = ( process.env.TI_AUDITING_LOG_USES_JSON !== undefined ) ? tools.toBool( process.env.TI_AUDITING_LOG_USES_JSON ) : settings.auditing.logUsesJSON;
140
142
  }
141
143
  if ( settings.memoryCache ) {
package/utils/logger.js CHANGED
@@ -41,6 +41,22 @@ module.exports.getSeverityName = ( severity ) => {
41
41
  return tools.getEnumName( logSeverityEnum, severity, "unknown" );
42
42
  };
43
43
 
44
+ /**
45
+ * Used to extract information from an Exception and convert it to loggable data object.
46
+ *
47
+ * @method
48
+ * @param {Exception} exception
49
+ * @returns {{description, details: (*|undefined), exceptionID}}
50
+ * @private
51
+ */
52
+ const exceptionToLog = ( exception ) => {
53
+ return {
54
+ exceptionID: exception.id,
55
+ description: exception.description,
56
+ details: !_.isEmpty( exception.data ) ? exception.data : undefined
57
+ };
58
+ };
59
+
44
60
  /**
45
61
  * Used to generate and store a log entry in the active cache.
46
62
  *
@@ -58,11 +74,9 @@ module.exports.log = ( message, level = logSeverityEnum.DEFAULT, data = {}, thre
58
74
  if ( data instanceof Error ) {
59
75
  data = tools.errorToJSON( data );
60
76
  } else if ( exceptions.isException( data ) ) {
61
- data = {
62
- id: data.id,
63
- description: data.description,
64
- details: !_.isEmpty( data.data ) ? data.data : undefined
65
- };
77
+ data = exceptionToLog( data );
78
+ } else if ( data.exception !== undefined && exceptions.isException( data.exception ) ) {
79
+ data.exception = exceptionToLog( data.exception );
66
80
  }
67
81
 
68
82
  auditing.log( message, level, thread, data );