@ti-engine/core 1.6.1 → 1.7.2

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +383 -364
  2. package/LICENSE.md +321 -321
  3. package/README.md +597 -548
  4. package/bin/localization/labels.json +122 -122
  5. package/bin/settings.json +41 -41
  6. package/bin/start-instance.js +164 -156
  7. package/components/auditing.js +191 -191
  8. package/components/connection-observer.js +72 -72
  9. package/components/definitions.types.js +248 -248
  10. package/components/exchange/default/default-message-exchange.js +136 -136
  11. package/components/exchange/default/default-message-receiver.js +101 -101
  12. package/components/exchange/default/default-message-sender.js +100 -100
  13. package/components/exchange/message-dispatcher.js +168 -168
  14. package/components/exchange/message-exchange.js +449 -449
  15. package/components/exchange/message-handler.js +235 -234
  16. package/components/exchange/message-memory-cache.js +190 -190
  17. package/components/exchange/message-observer.js +126 -126
  18. package/components/exchange/message-receiver.js +181 -181
  19. package/components/exchange/message-sender.js +143 -143
  20. package/components/exchange/message-tracer.js +212 -212
  21. package/components/service-caller.js +370 -370
  22. package/components/service-consumer.js +131 -131
  23. package/components/service-executor.js +278 -278
  24. package/components/service-instance.js +316 -316
  25. package/components/service-provider.js +251 -251
  26. package/integrations/redis-integration.js +591 -591
  27. package/package.json +89 -90
  28. package/utils/cache.js +772 -772
  29. package/utils/config.js +103 -103
  30. package/utils/exceptions.js +368 -368
  31. package/utils/localization.js +298 -298
  32. package/utils/logger.js +82 -82
  33. package/utils/tools.js +632 -632
package/README.md CHANGED
@@ -1,549 +1,598 @@
1
- # ti-engine core
2
-
3
- ![GitHub top language](https://img.shields.io/github/languages/top/Belleal/ti-engine)
4
- ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/Belleal/ti-engine)
5
- ![npms.io (scoped package)](https://img.shields.io/npms-io/maintenance-score/%40ti-engine/core)
6
- ![npms.io (scoped package)](https://img.shields.io/npms-io/popularity-score/%40ti-engine/core)
7
- ![npms.io (scoped package)](https://img.shields.io/npms-io/quality-score/%40ti-engine/core)
8
-
9
- ![Logo](https://raw.githubusercontent.com/Belleal/ti-engine/master/packages/core/docs/ti-engine-icon.ico)
10
-
11
- Flexible framework for the creation of microservices with [node.js](https://nodejs.org/).
12
-
13
- ## Introduction
14
-
15
- 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.
16
-
17
- ## Why ti-engine?
18
-
19
- The framework is created based on a decade of professional experience with the utilized technologies and architectural approach. Its primary goal is to provide you with a lightweight and flexible solution that can help you quickly build a microservice ecosystem with any degree of size and complexity.
20
-
21
- This is what you gain by using **ti-engine** in your project:
22
-
23
- * **Simplicity**: Begin productive work within minutes and get to codding your business logic
24
- * **Flexibility**: Go as complex as you need to in your implementation
25
- * **Reliability**: Message exchange between the services is constantly tracked across the entire ecosystem
26
- * **Security**: Messages are encrypted in transit and cannot be modified by external agents
27
- * **Scalability**: Serve mullions of requests by multiplying stateless service instances (hardware limitations still apply)
28
- * **Containerization**: Go with containers from the very start as the framework is designed to work in such an environment
29
-
30
- 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.
31
-
32
- ## Prerequisites & installation
33
-
34
- 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. For now let's focus on the default setup.
35
-
36
- To run the basic **ti-engine** framework, you will need a couple of things:
37
-
38
- * A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **18.0.0**
39
- * A local or remote [Redis cache installation](https://redis.io/download) with a minimum version of **5.0.14**
40
-
41
- If you are working under Windows 10+ OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/). You could also use [Redis Cloud](https://app.redislabs.com/) for development as it offers a free basic account.
42
-
43
- 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`.
44
-
45
- ## Getting started
46
-
47
- 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 crucial things while working with this framework:
48
-
49
- 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.
50
- 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()` to resolve the file paths). Be mindful of that whenever you declare relative file paths in the various settings.
51
-
52
- ### Running the tester module
53
-
54
- 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:
55
-
56
- 1. Open a command prompt and navigate to the directory of the tester module; it should be something like that:
57
- `[path to your project]/node_modules/@ti-engine/tester`
58
- 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. Also, this configuration assumes that you have an unprotected local Redis server running on 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 [Configuring for a remote Redis server](#configuring-for-a-remote-redis-server).
59
- 3. If everything was done properly, you should see the following output:
60
-
61
- ```text
62
- [timestamp]: [instance-id] - notice - Starting new instance of type 'ti-tester-service' with instance ID '[instance-id]'.
63
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'system-cache' and is ready to be used.
64
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-out' and is ready to be used.
65
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-in' and is ready to be used.
66
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-out' and is ready to be used.
67
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-in' and is ready to be used.
68
- [timestamp]: [instance-id] - info - Starting service registration process. There is NO default service handler provided.
69
- [timestamp]: [instance-id] - info - Registration of defined services completed with 2 successful out of 2 total.
70
- [timestamp]: [instance-id] - notice - Instance '[instance-id]' started successfully.
71
- » {"nodeVersion":[node-version],"operationMode":[mode]}
72
- [timestamp]: [instance-id] - info - Execution of 'Test 1: Service call to a simple service without chained services' successful.
73
- » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp]}}
74
- [timestamp]: [instance-id] - info - Execution of 'Test 2: Service call to a simple service with one chained service' successful.
75
- » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
76
- [timestamp]: [instance-id] - info - Execution of 'Test 3: Service call to a non-existent service' successful.
77
- » {"isSuccessful":false,"exception":{"exceptionID":[exception-id],"description":"The specified service is not found in the service registry."}}
78
- [timestamp]: [instance-id] - notice - All service tests completed. Passed 3 out of 3.
79
- ```
80
-
81
- Now let's analyze 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.
82
-
83
- At the start of the output log, you can see a `NOTICE` that tells you a couple of important things:
84
-
85
- * The instance name - in this case `ti-tester-service`. In the terminology of the framework, this is also known as a _service domain name_.
86
- * The _instance identifier_. 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.
87
-
88
- The following five `INFO` lines inform you about the successful connections to Redis. Remember, the default configuration assumes that your Redis is running on localhost, requires no password, and uses the default port.
89
-
90
- Following that come a couple of `INFO` lines that inform you about the microservice interface state. The framework starts with the process of _business services_ registration within the service domain of the microservice `ti-tester-service` and successfully adds two 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).
91
-
92
- Once the initialization sequence has completed, the framework informs you that the microservice instance has started successfully with a `NOTICE` entry.
93
-
94
- NOTE: If the framework encountered an error during initialization, you would see something like this instead:
95
-
96
- ```text
97
- [timestamp]: [instance-id] - notice - Starting new instance of type 'ti-tester-service' with instance ID '[instance-id]'.
98
- [timestamp]: [instance-id] - alert - Error detected in the instance startup script!
99
- » [information about the error]
100
- ```
101
-
102
- Finally, you should see a sequence of test execution statements with their results in JSON format. These are the results of three business service calls that are part of the default tester microservice. The final `NOTICE` should indicate that all three tests out of three have been completed successfully.
103
-
104
- You can now kill the node process which should show you the following two lines:
105
-
106
- ```text
107
- [timestamp]: [instance-id] - notice - [signal-code] event detected in main instance process.
108
- [timestamp]: [instance-id] - notice - Instance '[instance-id]' shut down successfully.
109
- ```
110
-
111
- The framework will always try to capture the shut-down event and log it with the correct `[signal-code]`. This should work even in a container environment, but it might depend on your setup whether the last two entries will reach the logging system or not.
112
-
113
- 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.
114
-
115
- Before moving on, also take a good look at the file `bin/start-instance.js`. It should give you an idea of how the process of starting and stopping a microservice operates. In most cases this file should be enough 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.
116
-
117
- ### Configuring for a remote Redis server
118
-
119
- You can configure your connection to a remote Redis server using the following ENV variables:
120
-
121
- * `TI_MEMORY_CACHE_AUTH_KEY` can be used to provide the Redis password if there is any at all.
122
- * `TI_MEMORY_CACHE_REDIS_DB` can be used to specify the Redis DB you want to use. Make sure to set the correct number as, for example, Redis Cloud only uses DB `0`.
123
- * `TI_MEMORY_CACHE_REDIS_HOST` can be used to provide the remote host. This can be an IP or URL depending on your setup.
124
- * `TI_MEMORY_CACHE_REDIS_PORT` can be used to provide the remote port. By default, Redis uses `6379` however many implementations might use a custom port that needs to be specified in the connection settings.
125
- * `TI_MEMORY_CACHE_USER` can be used to specify the Redis username if this is supported by your Redis implementation.
126
-
127
- The easiest way to configure these variables is to edit the `.env` file included in the package. You might have to do that if you want to run the tester module successfully with a remote Redis server.
128
-
129
- ## Architecture
130
-
131
- 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.
132
-
133
- There are three general tiers in the **ti-engine**:
134
-
135
- 1. Message exchange
136
- 2. Service domains
137
- 3. Solution implementation
138
-
139
- See the following sections for more information on each of them.
140
-
141
- ### Tier 1 - Message exchange
142
-
143
- 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 bonuses that can speed up your work—message encryption, message tracing, message observers, and others. More details about each of these features will be covered in the section [Using the framework](#using-the-framework).
144
-
145
- Another important aspect for you to remember is that the message exchange is entirely _asynchronous_. This helps reduce the system load and optimizes the usage of the available resources. Even so, each node.js process can handle a limited load. Therefore, you should plan for running multiple identical senders and receives to scale your solution. But more on that later.
146
-
147
- For now, take a look at the following diagram:
148
-
149
- ![Message Exchange](https://raw.githubusercontent.com/Belleal/ti-engine/master/packages/core/docs/diagram1.png)
150
-
151
- It shows the standard flow of a message exchange between one sender and _n_ identical message receivers. The sender splits each message into an _envelope_ and a _payload_, then stores the payload in the shared cache and enqueues the envelope in the requests (destination) queue. Receivers can subscribe to that queue to fetch enqueued messages and process their contents. During the fetch sequence a receiver assembles the full message by getting the payload from the storage. This process is depicted by the blue flow lines.
152
-
153
- 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.
154
-
155
- In this scenario the framework uses _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. The splitting between envelope and payload is done to avoid unnecessary transportation of potentially large volumes of operational data between the microservices. Other message brokers might use a slightly different approach, but they should still adhere to the same logical flow.
156
-
157
- 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:
158
-
159
- * Class `Message`: a message 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.
160
- * Class `MessageSender`: a 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.
161
- * Class `MessageReceiver`: a receiver is a specialized connector that is responsible for receiving messages at a predefined destination.
162
- * Class `MessageExchange`: the exchange is the actual message processing engine. It handles sending and receiving messages via preconfigured message senders and message receivers.
163
- * Class `MessageObserver`: an observer is a custom event listener that can be used to react on message `sent` and `received` events.
164
-
165
- ### Tier 2 - Service domains
166
-
167
- This tier focuses on hosting and executing the _business logic_ of your application. It consists 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_. All microservices are based on the `ServiceInstance` class, which establishes the basic framework structure and provides the basic functionality for the microservice lifecycle. It should not be used directly, however. Instead, there are two child types of `ServiceInstance` in **ti-engine** that you should use to implement your solution:
168
-
169
- * Class `ServiceConsumer`: consumers are service instances that can call business services in any connected and available service domain.
170
- * Class `ServiceProvider`: providers 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.
171
-
172
- 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, completely change the tier 1 approach without having to modify anything in your business logic and business flow.
173
-
174
- This tier is the place to use 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 the section [Using the framework](#using-the-framework).
175
-
176
- ### Tier 3 - Solution implementation
177
-
178
- This tier comprises the actual implementation of your application. Its structure and behavior depend entirely on your vision and business goals. There are still a couple of points that remain constant while using **ti-engine**:
179
-
180
- * It needs to use the business logic defined in tier 2 by calling the business services.
181
- * It needs to take care of any type of stateful behavior like user sessions or transactions.
182
- * It needs to act as the primary interface between users and your application, thus handling access management and user interactions.
183
-
184
- 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.
185
-
186
- ## Creating a microservice
187
-
188
- 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`.
189
-
190
- ### Deconstructing the ti-tester microservice
191
-
192
- If you managed to execute the initial framework test as explained in the [Running the tester module](#running-the-tester-module) section, you should already be familiar with the default ti-tester microservice. Here we'll dissect its contents even further.
193
-
194
- Let's take a look at the files and file structure first (only relevant items are shown):
195
-
196
- ```text
197
- bin
198
- services
199
- ↳ v1
200
- ↳ service1.js
201
- ↳ service2.js
202
- more-labels.json
203
- tester-service.js
204
- tester-service.json
205
- .env
206
- package.json
207
- ```
208
-
209
- 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.
210
-
211
- In the tester module 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:
212
-
213
- #### package.json contents
214
-
215
- ```json
216
- {
217
- "name": "@ti-engine/tester",
218
- "version": "[tester module version]",
219
- "description": "[tester module description]",
220
- "author": "[tester module author]",
221
- "license": "GPL-3.0-or-later",
222
- "dependencies": {
223
- "@ti-engine/core": "latest"
224
- },
225
- "engines": {
226
- "node": "[min required node version]"
227
- }
228
- }
229
- ```
230
-
231
- Apart from the standard information properties there are only two important entries here: `"@ti-engine/core": "[min required core version]"` and `"node": "[min required node version]"`. 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 the **ti-engine** itself.
232
-
233
- #### .env contents
234
-
235
- ```text
236
- TI_INSTANCE_NAME=ti-tester-service
237
- TI_INSTANCE_CLASS=bin/tester-service.js
238
- TI_INSTANCE_CONFIG=bin/tester-service.json
239
- TI_AUDITING_LOG_MIN_LEVEL=200
240
- TI_LOCALIZATION_LABELS_PATH=bin/more-labels.json
241
- ```
242
-
243
- 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 two are provided for the needs of the tester demonstration. Let's review them and see what they do:
244
-
245
- * `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.
246
- * `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.js` 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.
247
- * `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.
248
- * `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 tester microservice.
249
- * `TI_LOCALIZATION_LABELS_PATH` specifies the relative path to the additional localization labels file. This is optional and can be omitted if you don't need custom labels for your microservice.
250
-
251
- You can find the full list of available ENV variables and what they do below in [Using the framework](#using-the-framework) section.
252
-
253
- #### Application-specific files
254
-
255
- 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 a few methods that contain its behavior:
256
-
257
- * 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.
258
- * Method `reportHealthy` overrides but essentially just calls the same base method. Its only purpose here is to draw your attention to its existence and the possibility to implement your own health status reporting functionality if you want.
259
- * 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.
260
- * Method `#executeTests` is a custom private method that contains the test execution sequence itself. It is called by the `onStart` method just once per microservice start.
261
- * Method `#assertService` is an assertion wrapper around a standard service call request. Inside it, you can see how a business service is invoked and how the results should be handled.
262
-
263
- 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 the section [Using the framework](#using-the-framework). For now pay attention to the `serviceFile` parameter and that it once again provides a relative path to the actual file containing the business logic.
264
-
265
- The file `more-labels.json` contains one additional localization label for the tester microservice and is only used to demonstrate how to add custom labels to the microservice. For more information on localization, see the section [Localization](#localization).
266
-
267
- 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 basic 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 the section [Using the framework](#using-the-framework).
268
-
269
- ### Creating your own microservice
270
-
271
- 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 the same 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:
272
-
273
- ```text
274
- TI_INSTANCE_NAME=my-service
275
- TI_INSTANCE_CLASS=bin/my-service.js
276
- TI_INSTANCE_CONFIG=bin/my-service.json
277
- TI_AUDITING_LOG_MIN_LEVEL=200
278
- ```
279
-
280
- 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:
281
-
282
- ```js
283
- const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
284
-
285
- class MyService extends ServiceConsumer {}
286
-
287
- module.exports = MyService;
288
- ```
289
-
290
- Remember to also export your new class at the end, otherwise the framework won't be able to initialize it.
291
-
292
- 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:
293
-
294
- ```js
295
- const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
296
-
297
- class MyService extends ServiceConsumer {
298
- constructor( serviceDomainName, serviceConfig ) {
299
- super( serviceDomainName, serviceConfig );
300
- }
301
- }
302
-
303
- module.exports = MyService;
304
- ```
305
-
306
- And now let's add a service call executed at microservice start after `500` milliseconds timeout:
307
-
308
- ```js
309
- const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
310
- const logger = require( "@ti-engine/core/logger" );
311
- const exceptions = require( "@ti-engine/core/exceptions" );
312
- const { setTimeout: setTimeoutPromise } = require( "node:timers/promises" );
313
-
314
- class MyService extends ServiceConsumer {
315
- constructor( serviceDomainName, serviceConfig ) {
316
- super( serviceDomainName, serviceConfig );
317
- }
318
-
319
- onStart() {
320
- return new Promise( ( resolve, reject ) => {
321
- super.onStart().then( () => {
322
- return setTimeoutPromise( 500 );
323
- } ).then( () => {
324
- return this.callService( {
325
- serviceAlias: "service2",
326
- serviceDomainName: "ti-tester-service"
327
- }, {}, {
328
- authToken: "auth"
329
- } );
330
- } ).then( ( result ) => {
331
- logger.log( "Execution of service2 result:", logger.logSeverity.NOTICE, result );
332
- resolve();
333
- } ).catch( ( error ) => {
334
- reject( exceptions.raise( error ) );
335
- } );
336
- } );
337
- }
338
- }
339
-
340
- module.exports = MyService;
341
- ```
342
-
343
- 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:
344
-
345
- ```text
346
- [timestamp]: [instance-id] - notice - Starting new instance of type 'my-service' with instance ID '[instance-id]'.
347
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'system-cache' and is ready to be used.
348
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-in' and is ready to be used.
349
- [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-out' and is ready to be used.
350
- ```
351
-
352
- If you haven't started anything else, this is all you should see at this point.
353
-
354
- 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:
355
-
356
- ```text
357
- ...
358
- [timestamp]: [instance-id] - notice - Instance [instance-id] started successfully.
359
- » {"nodeVersion":[node-version]}
360
- [timestamp]: [instance-id] - notice - Execution of service2 result:
361
- » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
362
- ```
363
-
364
- This means the service call processing was successful and a 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`.
365
-
366
- And with this step we are done. The new microservice is now operational. You can continue to tweak and play with it to understand better how it all works. For more details on the **ti-engine** inner working, please see the following sections.
367
-
368
- ## Using the framework
369
-
370
- ### Environment variables
371
-
372
- These are the system-level ENV variables that can be used to configure the framework. Use an `.env` file or corresponding containerization features to set the variables you need. Not all of these are mandatory or even required for every microservice you create. Consult the documentation for more details.
373
-
374
- TI_AUDITING_LOG_MIN_LEVEL
375
- : Type: Optional
376
- : Specifies the minimum log level that should be sent to the log output stream. The framework will filter out all log entries with a level below this value. The default value is `200` (corresponding to INFO).
377
-
378
- TI_FAIL_FAST_ON_UNHANDLED_OFF
379
- : Type: Optional
380
- : Specifies whether the framework fail-fast on unhandled promise behavior should be turned off. Set this to `true` to disable the default behavior. The default value is `false`.
381
-
382
- TI_INSTANCE_CLASS
383
- : Type: Required
384
- : 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.
385
-
386
- TI_INSTANCE_CONFIG
387
- : Type: Optional
388
- : Specifies the relative path to the configuration data for the microservice. 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.
389
-
390
- TI_INSTANCE_NAME
391
- : Type: Required
392
- : Specifies 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.
393
-
394
- ### Framework settings
395
-
396
- Here you can find all settings used by **ti-engine** together with information on what they do. They are defined inside the `config` module and the full list can be accessed through the public `setting` enum. To get the current value of a setting, you can use the public method `getSetting` from the same module. Some settings can be overridden by providing ENV variables as specified below at node application startup.
397
-
398
- AUDITING_LOG_CONSOLE_ENABLED
399
- : JSON path `auditing.logConsoleEnabled`, type `boolean`, default `true`
400
- : ENV variable `TI_AUDITING_LOG_CONSOLE_ENABLED`
401
- : This setting controls whether the `auditing` module will send the log entries to the OS console or not. In some cases, like Cloud environments, you might want to disable this, especially if the OS console is not made available. This functions independently of other logging outputs like, for example, GCloud error reporting.
402
-
403
- AUDITING_LOG_DETAILS
404
- : JSON path `auditing.logDetails`, type `boolean`, default `true`
405
- : ENV variable `TI_AUDITING_LOG_DETAILS`
406
- : This setting controls whether the `auditing` module will include the log entry details (located in the `data` property) in the final log output. You might want to disable this if you want a leaner log output, or the log entry details are not something you plan to use for analysis later.
407
-
408
- AUDITING_LOG_MIN_LEVEL
409
- : JSON path `auditing.logMinLevel`, type `number`, default `0`
410
- : ENV variable `TI_AUDITING_LOG_MIN_LEVEL`
411
- : This setting controls the minimum log severity level that the framework will log in the log output. You can and should set this to `200` (INFO) for production environments to filter out the DEBUG and the low-level DEFAULT entries.
412
-
413
- AUDITING_LOG_USES_JSON
414
- : JSON path `auditing.logUsesJSON`, type `boolean`, default `false`
415
- : ENV variable `TI_AUDITING_LOG_USES_JSON`
416
- : This setting controls whether the log entries would be sent to output formatted as JSONs or not. By default, the framework outputs log entries as prettified text. In some cases, however, you might want to have the entire entry as a JSON for further processing (for example, if you're sending all logs to Elasticsearch).
417
-
418
- GCLOUD_API_KEY (Alpha)
419
- : JSON path `gcloudIntegration.apiKey`
420
- : This setting holds the API key for the GCloud integration module.
421
-
422
- GCLOUD_PROJECT_ID (Alpha)
423
- : JSON path `gcloudIntegration.projectID`
424
- : This setting holds the project ID for the GCloud integration module.
425
-
426
- LOCALIZATION_LABELS_PATH
427
- : JSON path `localization.labelsPath`, type `Array<string>`, default `[]`
428
- : ENV variable `TI_LOCALIZATION_LABELS_PATH`
429
- : This setting holds a list of paths to custom `.json` files containing additional localization information. By default, the framework also provides such a file with English texts that can be customized further. All additional JSONs in these files have to follow the rules and structure of the `localization` module. The ENV variable currently supports providing only a single custom path.
430
-
431
- LOCALIZATION_LANGUAGE
432
- : JSON path `localization.language`, type `string`, default `en`
433
- : ENV variable `TI_LOCALIZATION_LANGUAGE`
434
- : This setting specifies the default framework language. It will be used when translating labels into a localized text.
435
-
436
- MEMORY_CACHE_AUTH_KEY
437
- : JSON path `memoryCache.authKey`, type `string`, default `undefined`
438
- : ENV variable `TI_MEMORY_CACHE_AUTH_KEY`
439
- : This setting holds the Redis password for accessing the Redis server if such a password is required.
440
-
441
- MEMORY_CACHE_REDIS_DB
442
- : JSON path `memoryCache.redisDB`, type `number`, default `0`
443
- : ENV variable `TI_MEMORY_CACHE_REDIS_DB`
444
- : This setting specifies the Redis DB to be used for all operations. When setting this make sure that the Redis server actually supports multiple DBs (for example, Redis Cloud has only one DB with ID `0`).
445
-
446
- MEMORY_CACHE_REDIS_HOST
447
- : JSON path `memoryCache.redisHost`, type `string`, default `127.0.0.1`
448
- : ENV variable `TI_MEMORY_CACHE_REDIS_HOST`
449
- : This setting holds the Redis server hostname. It can be an IP or URL depending on your configuration.
450
-
451
- MEMORY_CACHE_REDIS_PORT
452
- : JSON path `memoryCache.redisPort`, type `number`, default `6379`
453
- : ENV variable `TI_MEMORY_CACHE_REDIS_PORT`
454
- : This setting holds the Redis server port. By default, Redis uses `6379` however many implementations might use a custom port that needs to be specified in the connection settings.
455
-
456
- MEMORY_CACHE_RETRY_MAX_ATTEMPTS
457
- : JSON path `memoryCache.retryMaxAttempts`, type `number`, default `undefined`
458
- : ENV variable `TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS`
459
- : This setting holds the maximum number of attempts to (re)connect to the Redis server. By default, there is no limit.
460
-
461
- MEMORY_CACHE_RETRY_MAX_INTERVAL
462
- : JSON path `memoryCache.retryMaxInterval`, type `number`, default `1000`
463
- : ENV variable `TI_MEMORY_CACHE_RETRY_MAX_INTERVAL`
464
- : This setting holds the maximum interval in milliseconds between retries when the Redis server is not available.
465
-
466
- MEMORY_CACHE_USER
467
- : JSON path `memoryCache.user`, type `string`, default `default`
468
- : ENV variable `TI_MEMORY_CACHE_USER`
469
- : This setting holds the Redis username for accessing the Redis server if this is supported by the Redis version (it will be ignored otherwise).
470
-
471
- MESSAGE_EXCHANGE_QUEUE_PREFIX (Advanced)
472
- : JSON path `messageExchange.messageQueuePrefix`, type `string`, default `ti:messages:`
473
- : This setting holds the Redis key prefix for the queues that will hold the messages of the message exchange. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
474
-
475
- MESSAGE_EXCHANGE_MESSAGE_STORE (Advanced)
476
- : JSON path `messageExchange.messageStore`, type `string`, default `ti:messages:store`
477
- : This setting holds the Redis key name of the hash table that will hold the message payloads of the message exchange. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
478
-
479
- MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED (Advanced)
480
- : JSON path `messageExchange.securityHashEnabled`, type `boolean`, default `true`
481
- : ENV variable `TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED`
482
- : This setting controls whether the message exchange will use a control hash mechanism to ensure there is no tampering with the messages in between service calls. In most cases you would want to keep this enabled since it ensures the integrity of your data. If you are concerned about performance (the keyed `HMAC-SHA256` hash is rapid, but it still eats some milliseconds) you might want to try and disable this to see if it makes any notable difference.
483
-
484
- MESSAGE_EXCHANGE_SECURITY_HASH_KEY (Advanced)
485
- : JSON path `messageExchange.securityHashKey`, type `string`, default `empty (must be set)`
486
- : ENV variable `TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY`
487
- : This setting holds the HMAC key used by the message exchange control hash mechanism. By default it is empty, which leaves tamper protection ineffective and logs a startup warning. You absolutely must provide your own private key via the ENV variable in any real deployment. Depending on your configuration and infrastructure, it might come from a secure storage, HSM, key vault, etc.
488
-
489
- MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME
490
- : JSON path `messageExchange.traceExpirationTime`, type `number`, default `3600`
491
- : This setting specifies the expiration time in seconds of the Redis key that will hold the message trace entries. Set this to `0` to disable expiration altogether.
492
-
493
- MESSAGE_EXCHANGE_TRACE_LOG_ENABLED
494
- : JSON path `messageExchange.traceLogEnabled`, type `boolean`, default `false`
495
- : ENV variable `TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED`
496
- : This setting controls whether the `auditing` module should output all trace messages as normal log entries or not. Normally, you don't want that since it will clutter the standard log quite a lot. All traces go to their own storage and can be reviewed and processed separately from the log entries. In some cases, however, as in debugging, enabling this can help you identify a hard to track problem.
497
-
498
- MESSAGE_EXCHANGE_TRACE_REPOSITORY (Advanced)
499
- : JSON path `messageExchange.traceRepository`, type `string`, default `ti:messages:trace`
500
- : This setting holds the Redis key name for the message trace cache storage. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
501
-
502
- SERVICE_EXECUTION_TIMEOUT
503
- : JSON path `serviceConfig.executionTimeout`, type `number`, default `180000`
504
- : This setting specifies the timeout in milliseconds of the service call executions at tier 2 of the architecture. Any service call that hasn't received response within this time will interrupt the wait and raise an `E_COM_SERVICE_EXEC_TIMEOUT` exception. Please keep in mind that reaching the timeout does not mean the remote service did not process the request. You might want to tweak this setting if you have many time-consuming operations in business services, or you plan to integrate with slow APIs.
505
-
506
- SERVICE_HEALTH_CHECK_ADDRESS (Advanced)
507
- : JSON path `serviceConfig.healthCheckAddress`, type `string`, default `ti:services:registry:health:`
508
- : This setting specifies the address of the health check report endpoint for the microservice. In the default implementation this is a prefix for a Redis key that gets updated once at every `SERVICE_HEALTH_CHECK_INTERVAL`. If you override the `reportHealthy` method of the microservice, this setting can contain a URL or another type of destination that can be used by your custom implementation.
509
-
510
- SERVICE_HEALTH_CHECK_INTERVAL (Advanced)
511
- : JSON path `serviceConfig.healthCheckInterval`, type `CRON string`, default `*/1 * * * * *`
512
- : This setting specifies the health check report interval at which the endpoint in `SERVICE_HEALTH_CHECK_ADDRESS` is notified.
513
-
514
- SERVICE_HEALTH_CHECK_TIMEOUT (Advanced)
515
- : JSON path `serviceConfig.healthCheckTimeout`, type `number`, default `3`
516
- : This setting specifies the timeout in seconds after which a microservice is no longer considered healthy. In the default implementation this represents an expiration parameter to the Redis key defined in `SERVICE_HEALTH_CHECK_ADDRESS`. Essentially, if the microservice does not update the Redis key within this time interval, it will expire and the monitoring application will lose the healthy status of the microservice. If you override the `reportHealthy` method of the microservice, this setting can be used for your custom implementation as needed.
517
-
518
- SERVICE_REGISTRY_ADDRESS (Advanced)
519
- : JSON path `serviceConfig.serviceRegistryAddress`, type `string`, default `ti:services:registry:catalog:`
520
- : This setting holds the prefix of the Redis key name used as a business service registry. If the microservice is a `ServiceProvider`, on start up it will register its business service portfolio in that Redis set. Also, on each service call that same registry will be searched for the existence of the called business service. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
521
-
522
- OPERATION_MODE
523
- : JSON path `operationMode`, type `string`, default `production`
524
- : ENV variable `NODE_ENV`
525
- : This setting holds the current operation mode of the node application. It will inherit the value from the `NODE_ENV` variable if it exists, otherwise will use its default.
526
-
527
- ## Advanced topics
528
-
529
- ### Localization
530
-
531
- The **ti-engine** framework provides a localization mechanism that allows you to translate labels into localized text. The framework comes with a default set of labels that can be found in the `localization` module. You can add your own custom labels to this set by providing one or more JSON files with the same structure as the default one (see below). The path to these files should be specified in the `LOCALIZATION_LABELS_PATH` setting. On startup, the framework will load all the JSON files and merge them into a single repository.
532
-
533
- The following is an example of a custom localization file. The names in brackets can be replaced with your own values. The depth of the three is unlimited and can be used to create a hierarchy of labels.
534
-
535
- ```json
536
- {
537
- "[category]": {
538
- "[sub-category]": {
539
- "[label]": {
540
- "[language code]": "[localized label text]"
541
- }
542
- }
543
- }
544
- }
545
- ```
546
-
547
- The individual languages are specified with a two-letter code according to ISO 639-1. The default language is `en` (English). If you want to use a different language, you can set the `LOCALIZATION_LANGUAGE` setting to the desired language code.
548
-
1
+ # ti-engine core
2
+
3
+ ![GitHub top language](https://img.shields.io/github/languages/top/Belleal/ti-engine)
4
+ ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/Belleal/ti-engine)
5
+ ![npms.io (scoped package)](https://img.shields.io/npms-io/maintenance-score/%40ti-engine/core)
6
+ ![npms.io (scoped package)](https://img.shields.io/npms-io/popularity-score/%40ti-engine/core)
7
+ ![npms.io (scoped package)](https://img.shields.io/npms-io/quality-score/%40ti-engine/core)
8
+
9
+ ![Logo](https://raw.githubusercontent.com/Belleal/ti-engine/master/packages/core/docs/ti-engine-icon.ico)
10
+
11
+ Flexible framework for the creation of microservices with [node.js](https://nodejs.org/).
12
+
13
+ ## Introduction
14
+
15
+ 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.
16
+
17
+ ## Why ti-engine?
18
+
19
+ The framework is created based on a decade of professional experience with the utilized technologies and architectural approach. Its primary goal is to provide you with a lightweight and flexible solution that can help you quickly build a microservice ecosystem with any degree of size and complexity.
20
+
21
+ This is what you gain by using **ti-engine** in your project:
22
+
23
+ * **Simplicity**: Begin productive work within minutes and get to codding your business logic
24
+ * **Flexibility**: Go as complex as you need to in your implementation
25
+ * **Reliability**: Message exchange between the services is constantly tracked across the entire ecosystem
26
+ * **Security**: Messages are encrypted in transit and cannot be modified by external agents
27
+ * **Scalability**: Serve mullions of requests by multiplying stateless service instances (hardware limitations still apply)
28
+ * **Containerization**: Go with containers from the very start as the framework is designed to work in such an environment
29
+
30
+ 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.
31
+
32
+ ## Prerequisites & installation
33
+
34
+ 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. For now let's focus on the default setup.
35
+
36
+ To run the basic **ti-engine** framework, you will need a couple of things:
37
+
38
+ * A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **20.12.0** (the `core` package requires `process.loadEnvFile`)
39
+ * A local or remote [Redis cache installation](https://redis.io/download) with a minimum version of **5.0.14**
40
+
41
+ If you are working under Windows 10+ OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/). You could also use [Redis Cloud](https://app.redislabs.com/) for development as it offers a free basic account.
42
+
43
+ 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`.
44
+
45
+ ## Getting started
46
+
47
+ 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 crucial things while working with this framework:
48
+
49
+ 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.
50
+ 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()` to resolve the file paths). Be mindful of that whenever you declare relative file paths in the various settings.
51
+
52
+ ### Running the tester module
53
+
54
+ 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:
55
+
56
+ 1. Open a command prompt and navigate to the directory of the tester module; it should be something like that:
57
+ `[path to your project]/node_modules/@ti-engine/tester`
58
+ 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. Also, this configuration assumes that you have an unprotected local Redis server running on 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 [Configuring for a remote Redis server](#configuring-for-a-remote-redis-server).
59
+ 3. If everything was done properly, you should see the following output:
60
+
61
+ ```text
62
+ [timestamp]: [instance-id] - notice - Starting new instance of type 'ti-tester-service' with instance ID '[instance-id]'.
63
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'system-cache' and is ready to be used.
64
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-out' and is ready to be used.
65
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-in' and is ready to be used.
66
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-out' and is ready to be used.
67
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-in' and is ready to be used.
68
+ [timestamp]: [instance-id] - info - Starting service registration process. There is NO default service handler provided.
69
+ [timestamp]: [instance-id] - info - Registration of defined services completed with 2 successful out of 2 total.
70
+ [timestamp]: [instance-id] - notice - Instance '[instance-id]' started successfully.
71
+ » {"nodeVersion":[node-version],"operationMode":[mode]}
72
+ [timestamp]: [instance-id] - info - Execution of 'Test 1: Service call to a simple service without chained services' successful.
73
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp]}}
74
+ [timestamp]: [instance-id] - info - Execution of 'Test 2: Service call to a simple service with one chained service' successful.
75
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
76
+ [timestamp]: [instance-id] - info - Execution of 'Test 3: Service call to a non-existent service' successful.
77
+ » {"isSuccessful":false,"exception":{"exceptionID":[exception-id],"description":"The specified service is not found in the service registry."}}
78
+ [timestamp]: [instance-id] - notice - All service tests completed. Passed 3 out of 3.
79
+ ```
80
+
81
+ Now let's analyze 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.
82
+
83
+ At the start of the output log, you can see a `NOTICE` that tells you a couple of important things:
84
+
85
+ * The instance name - in this case `ti-tester-service`. In the terminology of the framework, this is also known as a _service domain name_.
86
+ * The _instance identifier_. 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.
87
+
88
+ The following five `INFO` lines inform you about the successful connections to Redis. Remember, the default configuration assumes that your Redis is running on localhost, requires no password, and uses the default port.
89
+
90
+ Following that come a couple of `INFO` lines that inform you about the microservice interface state. The framework starts with the process of _business services_ registration within the service domain of the microservice `ti-tester-service` and successfully adds two 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).
91
+
92
+ Once the initialization sequence has completed, the framework informs you that the microservice instance has started successfully with a `NOTICE` entry.
93
+
94
+ NOTE: If the framework encountered an error during initialization, you would see something like this instead:
95
+
96
+ ```text
97
+ [timestamp]: [instance-id] - notice - Starting new instance of type 'ti-tester-service' with instance ID '[instance-id]'.
98
+ [timestamp]: [instance-id] - alert - Error detected in the instance startup script!
99
+ » [information about the error]
100
+ ```
101
+
102
+ Finally, you should see a sequence of test execution statements with their results in JSON format. These are the results of three business service calls that are part of the default tester microservice. The final `NOTICE` should indicate that all three tests out of three have been completed successfully.
103
+
104
+ You can now kill the node process which should show you the following two lines:
105
+
106
+ ```text
107
+ [timestamp]: [instance-id] - notice - [signal-code] event detected in main instance process.
108
+ [timestamp]: [instance-id] - notice - Instance '[instance-id]' shut down successfully.
109
+ ```
110
+
111
+ The framework will always try to capture the shut-down event and log it with the correct `[signal-code]`. This should work even in a container environment, but it might depend on your setup whether the last two entries will reach the logging system or not.
112
+
113
+ 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.
114
+
115
+ Before moving on, also take a good look at the file `bin/start-instance.js`. It should give you an idea of how the process of starting and stopping a microservice operates. In most cases this file should be enough 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.
116
+
117
+ ### Configuring for a remote Redis server
118
+
119
+ You can configure your connection to a remote Redis server using the following ENV variables:
120
+
121
+ * `TI_MEMORY_CACHE_AUTH_KEY` can be used to provide the Redis password if there is any at all.
122
+ * `TI_MEMORY_CACHE_REDIS_DB` can be used to specify the Redis DB you want to use. Make sure to set the correct number as, for example, Redis Cloud only uses DB `0`.
123
+ * `TI_MEMORY_CACHE_REDIS_HOST` can be used to provide the remote host. This can be an IP or URL depending on your setup.
124
+ * `TI_MEMORY_CACHE_REDIS_PORT` can be used to provide the remote port. By default, Redis uses `6379` however many implementations might use a custom port that needs to be specified in the connection settings.
125
+ * `TI_MEMORY_CACHE_USER` can be used to specify the Redis username if this is supported by your Redis implementation.
126
+
127
+ The easiest way to configure these variables is to edit the `.env` file included in the package. You might have to do that if you want to run the tester module successfully with a remote Redis server.
128
+
129
+ ## Architecture
130
+
131
+ 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.
132
+
133
+ There are three general tiers in the **ti-engine**:
134
+
135
+ 1. Message exchange
136
+ 2. Service domains
137
+ 3. Solution implementation
138
+
139
+ See the following sections for more information on each of them.
140
+
141
+ ### Tier 1 - Message exchange
142
+
143
+ 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 bonuses that can speed up your work—message integrity hashing, message tracing, message observers, and others. More details about each of these features will be covered in the section [Using the framework](#using-the-framework).
144
+
145
+ Another important aspect for you to remember is that the message exchange is entirely _asynchronous_. This helps reduce the system load and optimizes the usage of the available resources. Even so, each node.js process can handle a limited load. Therefore, you should plan for running multiple identical senders and receives to scale your solution. But more on that later.
146
+
147
+ The sequence below shows a full service-call round trip through the default Redis exchange. Each message is split into a lightweight **envelope** (metadata, carrying the integrity hash) and a **payload** (the operational data): the payload is parked in a shared Redis hash while only the envelope travels through the queue.
148
+
149
+ ```mermaid
150
+ sequenceDiagram
151
+ autonumber
152
+ participant Caller as ServiceConsumer<br/>(ServiceCaller)
153
+ participant SOut as MessageSender<br/>(requests-out)
154
+ participant Redis as Redis<br/>(list queues + payload hash)
155
+ participant RIn as MessageReceiver<br/>(requests-in)
156
+ participant Exec as ServiceProvider<br/>(ServiceExecutor)
157
+
158
+ rect rgb(232, 243, 255)
159
+ Note over Caller,Exec: Request path (blue)
160
+ Caller->>SOut: callService() builds the request envelope
161
+ SOut->>SOut: stamp HMAC-SHA256 hash over the message
162
+ SOut->>Redis: HSET payload into ti:messages:store (field = storeID)
163
+ SOut->>Redis: LPUSH envelope onto ti:messages:pending:{destination}
164
+ RIn->>Redis: BRPOP ti:messages:pending:{domain} (blocks until a message)
165
+ Redis-->>RIn: envelope (payload = storeID)
166
+ RIn->>Redis: HGET + HDEL storeID from ti:messages:store
167
+ Redis-->>RIn: payload, reassembled into the full message
168
+ RIn->>RIn: recompute HMAC, constant-time verify
169
+ RIn->>Exec: deliver the verified message
170
+ Exec->>Exec: resolve handler by alias + version, then run it
171
+ end
172
+
173
+ rect rgb(255, 235, 235)
174
+ Note over Caller,Exec: Response path (red, mirrored route)
175
+ Exec->>Redis: HSET result payload, LPUSH onto ti:messages:processed:{source}:{instanceID}
176
+ Caller->>Redis: BRPOP ti:messages:processed:{source}:{instanceID} (blocks)
177
+ Redis-->>Caller: response envelope, reassembled and verified
178
+ Caller->>Caller: resolve the awaiting Promise with the ServiceCallResult
179
+ end
180
+ ```
181
+
182
+ If the recomputed hash does not match the one on the envelope, the receiver rejects the message with `E_SEC_MESSAGE_TAMPERING_DETECTED` instead of delivering it.
183
+
184
+ The same queue can be consumed by _n_ identical receivers, which is how you scale a service domain horizontally. The sender splits each message into an _envelope_ and a _payload_, then stores the payload in the shared cache and enqueues the envelope in the requests (destination) queue. Receivers can subscribe to that queue to fetch enqueued messages and process their contents. During the fetch sequence a receiver assembles the full message by getting the payload from the storage. This is the blue (request) path in the diagram above.
185
+
186
+ 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 is the red (response) path in the diagram above.
187
+
188
+ In this scenario the framework uses _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. The splitting between envelope and payload is done to avoid unnecessary transportation of potentially large volumes of operational data between the microservices. Other message brokers might use a slightly different approach, but they should still adhere to the same logical flow.
189
+
190
+ 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:
191
+
192
+ * Class `Message`: a message 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.
193
+ * Class `MessageSender`: a 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.
194
+ * Class `MessageReceiver`: a receiver is a specialized connector that is responsible for receiving messages at a predefined destination.
195
+ * Class `MessageExchange`: the exchange is the actual message processing engine. It handles sending and receiving messages via preconfigured message senders and message receivers.
196
+ * Class `MessageObserver`: an observer is a custom event listener that can be used to react on message `sent` and `received` events.
197
+
198
+ The exchange classes form the hierarchy below. `MessageHandler` is the shared abstract base that owns the integrity hash; the concrete `Default*` classes are the Redis implementation you replace to swap brokers:
199
+
200
+ ```mermaid
201
+ classDiagram
202
+ direction LR
203
+ MessageHandler <|-- MessageSender
204
+ MessageHandler <|-- MessageReceiver
205
+ MessageObserver <|-- MessageExchange
206
+ MessageSender <|-- DefaultMessageSender
207
+ MessageReceiver <|-- DefaultMessageReceiver
208
+ MessageExchange <|-- DefaultMessageExchange
209
+ class MessageHandler {
210
+ +createMessageHash()
211
+ }
212
+ ```
213
+
214
+ ### Tier 2 - Service domains
215
+
216
+ This tier focuses on hosting and executing the _business logic_ of your application. It consists 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_. All microservices are based on the `ServiceInstance` class, which establishes the basic framework structure and provides the basic functionality for the microservice lifecycle. It should not be used directly, however. Instead, there are two child types of `ServiceInstance` in **ti-engine** that you should use to implement your solution:
217
+
218
+ * Class `ServiceConsumer`: consumers are service instances that can call business services in any connected and available service domain.
219
+ * Class `ServiceProvider`: providers 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.
220
+
221
+ 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, completely change the tier 1 approach without having to modify anything in your business logic and business flow.
222
+
223
+ This tier is the place to use 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 the section [Using the framework](#using-the-framework).
224
+
225
+ ### Tier 3 - Solution implementation
226
+
227
+ This tier comprises the actual implementation of your application. Its structure and behavior depend entirely on your vision and business goals. There are still a couple of points that remain constant while using **ti-engine**:
228
+
229
+ * It needs to use the business logic defined in tier 2 by calling the business services.
230
+ * It needs to take care of any type of stateful behavior like user sessions or transactions.
231
+ * It needs to act as the primary interface between users and your application, thus handling access management and user interactions.
232
+
233
+ 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.
234
+
235
+ ## Creating a microservice
236
+
237
+ 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`.
238
+
239
+ ### Deconstructing the ti-tester microservice
240
+
241
+ If you managed to execute the initial framework test as explained in the [Running the tester module](#running-the-tester-module) section, you should already be familiar with the default ti-tester microservice. Here we'll dissect its contents even further.
242
+
243
+ Let's take a look at the files and file structure first (only relevant items are shown):
244
+
245
+ ```text
246
+ bin
247
+ services
248
+ v1
249
+ service1.js
250
+ ↳ service2.js
251
+ more-labels.json
252
+ ↳ tester-service.js
253
+ tester-service.json
254
+ .env
255
+ package.json
256
+ ```
257
+
258
+ 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.
259
+
260
+ In the tester module 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:
261
+
262
+ #### package.json contents
263
+
264
+ ```json
265
+ {
266
+ "name": "@ti-engine/tester",
267
+ "version": "[tester module version]",
268
+ "description": "[tester module description]",
269
+ "author": "[tester module author]",
270
+ "license": "GPL-3.0-or-later",
271
+ "dependencies": {
272
+ "@ti-engine/core": "latest"
273
+ },
274
+ "engines": {
275
+ "node": "[min required node version]"
276
+ }
277
+ }
278
+ ```
279
+
280
+ Apart from the standard information properties there are only two important entries here: `"@ti-engine/core": "[min required core version]"` and `"node": "[min required node version]"`. 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 the **ti-engine** itself.
281
+
282
+ #### .env contents
283
+
284
+ ```text
285
+ TI_INSTANCE_NAME=ti-tester-service
286
+ TI_INSTANCE_CLASS=bin/tester-service.js
287
+ TI_INSTANCE_CONFIG=bin/tester-service.json
288
+ TI_AUDITING_LOG_MIN_LEVEL=200
289
+ TI_LOCALIZATION_LABELS_PATH=bin/more-labels.json
290
+ ```
291
+
292
+ 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 two are provided for the needs of the tester demonstration. Let's review them and see what they do:
293
+
294
+ * `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.
295
+ * `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.js` 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.
296
+ * `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.
297
+ * `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 tester microservice.
298
+ * `TI_LOCALIZATION_LABELS_PATH` specifies the relative path to the additional localization labels file. This is optional and can be omitted if you don't need custom labels for your microservice.
299
+
300
+ You can find the full list of available ENV variables and what they do below in [Using the framework](#using-the-framework) section.
301
+
302
+ #### Application-specific files
303
+
304
+ 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 a few methods that contain its behavior:
305
+
306
+ * 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.
307
+ * Method `reportHealthy` overrides but essentially just calls the same base method. Its only purpose here is to draw your attention to its existence and the possibility to implement your own health status reporting functionality if you want.
308
+ * 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.
309
+ * Method `#executeTests` is a custom private method that contains the test execution sequence itself. It is called by the `onStart` method just once per microservice start.
310
+ * Method `#assertService` is an assertion wrapper around a standard service call request. Inside it, you can see how a business service is invoked and how the results should be handled.
311
+
312
+ 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 the section [Using the framework](#using-the-framework). For now pay attention to the `serviceFile` parameter and that it once again provides a relative path to the actual file containing the business logic.
313
+
314
+ The file `more-labels.json` contains one additional localization label for the tester microservice and is only used to demonstrate how to add custom labels to the microservice. For more information on localization, see the section [Localization](#localization).
315
+
316
+ 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 basic 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 the section [Using the framework](#using-the-framework).
317
+
318
+ ### Creating your own microservice
319
+
320
+ 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 the same 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:
321
+
322
+ ```text
323
+ TI_INSTANCE_NAME=my-service
324
+ TI_INSTANCE_CLASS=bin/my-service.js
325
+ TI_INSTANCE_CONFIG=bin/my-service.json
326
+ TI_AUDITING_LOG_MIN_LEVEL=200
327
+ ```
328
+
329
+ 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:
330
+
331
+ ```js
332
+ const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
333
+
334
+ class MyService extends ServiceConsumer {}
335
+
336
+ module.exports = MyService;
337
+ ```
338
+
339
+ Remember to also export your new class at the end, otherwise the framework won't be able to initialize it.
340
+
341
+ 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:
342
+
343
+ ```js
344
+ const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
345
+
346
+ class MyService extends ServiceConsumer {
347
+ constructor( serviceDomainName, serviceConfig ) {
348
+ super( serviceDomainName, serviceConfig );
349
+ }
350
+ }
351
+
352
+ module.exports = MyService;
353
+ ```
354
+
355
+ And now let's add a service call executed at microservice start after `500` milliseconds timeout:
356
+
357
+ ```js
358
+ const ServiceConsumer = require( "@ti-engine/core/service-consumer" );
359
+ const logger = require( "@ti-engine/core/logger" );
360
+ const exceptions = require( "@ti-engine/core/exceptions" );
361
+ const { setTimeout: setTimeoutPromise } = require( "node:timers/promises" );
362
+
363
+ class MyService extends ServiceConsumer {
364
+ constructor( serviceDomainName, serviceConfig ) {
365
+ super( serviceDomainName, serviceConfig );
366
+ }
367
+
368
+ onStart() {
369
+ return new Promise( ( resolve, reject ) => {
370
+ super.onStart().then( () => {
371
+ return setTimeoutPromise( 500 );
372
+ } ).then( () => {
373
+ return this.callService( {
374
+ serviceAlias: "service2",
375
+ serviceDomainName: "ti-tester-service"
376
+ }, {}, {
377
+ authToken: "auth"
378
+ } );
379
+ } ).then( ( result ) => {
380
+ logger.log( "Execution of service2 result:", logger.logSeverity.NOTICE, result );
381
+ resolve();
382
+ } ).catch( ( error ) => {
383
+ reject( exceptions.raise( error ) );
384
+ } );
385
+ } );
386
+ }
387
+ }
388
+
389
+ module.exports = MyService;
390
+ ```
391
+
392
+ 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:
393
+
394
+ ```text
395
+ [timestamp]: [instance-id] - notice - Starting new instance of type 'my-service' with instance ID '[instance-id]'.
396
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'system-cache' and is ready to be used.
397
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-in' and is ready to be used.
398
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-out' and is ready to be used.
399
+ ```
400
+
401
+ If you haven't started anything else, this is all you should see at this point.
402
+
403
+ 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:
404
+
405
+ ```text
406
+ ...
407
+ [timestamp]: [instance-id] - notice - Instance [instance-id] started successfully.
408
+ » {"nodeVersion":[node-version]}
409
+ [timestamp]: [instance-id] - notice - Execution of service2 result:
410
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
411
+ ```
412
+
413
+ This means the service call processing was successful and a 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`.
414
+
415
+ And with this step we are done. The new microservice is now operational. You can continue to tweak and play with it to understand better how it all works. For more details on the **ti-engine** inner working, please see the following sections.
416
+
417
+ ## Using the framework
418
+
419
+ ### Environment variables
420
+
421
+ These are the system-level ENV variables that can be used to configure the framework. Use an `.env` file or corresponding containerization features to set the variables you need. Not all of these are mandatory or even required for every microservice you create. Consult the documentation for more details.
422
+
423
+ TI_AUDITING_LOG_MIN_LEVEL
424
+ : Type: Optional
425
+ : Specifies the minimum log level that should be sent to the log output stream. The framework will filter out all log entries with a level below this value. The default value is `200` (corresponding to INFO).
426
+
427
+ TI_FAIL_FAST_ON_UNHANDLED_OFF
428
+ : Type: Optional
429
+ : Specifies whether the framework fail-fast on unhandled promise behavior should be turned off. Set this to `true` to disable the default behavior. The default value is `false`.
430
+
431
+ TI_INSTANCE_CLASS
432
+ : Type: Required
433
+ : 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.
434
+
435
+ TI_INSTANCE_CONFIG
436
+ : Type: Optional
437
+ : Specifies the relative path to the configuration data for the microservice. 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.
438
+
439
+ TI_INSTANCE_NAME
440
+ : Type: Required
441
+ : Specifies 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.
442
+
443
+ ### Framework settings
444
+
445
+ Here you can find all settings used by **ti-engine** together with information on what they do. They are defined inside the `config` module and the full list can be accessed through the public `setting` enum. To get the current value of a setting, you can use the public method `getSetting` from the same module. Some settings can be overridden by providing ENV variables as specified below at node application startup.
446
+
447
+ AUDITING_LOG_CONSOLE_ENABLED
448
+ : JSON path `auditing.logConsoleEnabled`, type `boolean`, default `true`
449
+ : ENV variable `TI_AUDITING_LOG_CONSOLE_ENABLED`
450
+ : This setting controls whether the `auditing` module will send the log entries to the OS console or not. In some cases, like Cloud environments, you might want to disable this, especially if the OS console is not made available. This functions independently of other logging outputs like, for example, GCloud error reporting.
451
+
452
+ AUDITING_LOG_DETAILS
453
+ : JSON path `auditing.logDetails`, type `boolean`, default `true`
454
+ : ENV variable `TI_AUDITING_LOG_DETAILS`
455
+ : This setting controls whether the `auditing` module will include the log entry details (located in the `data` property) in the final log output. You might want to disable this if you want a leaner log output, or the log entry details are not something you plan to use for analysis later.
456
+
457
+ AUDITING_LOG_MIN_LEVEL
458
+ : JSON path `auditing.logMinLevel`, type `number`, default `0`
459
+ : ENV variable `TI_AUDITING_LOG_MIN_LEVEL`
460
+ : This setting controls the minimum log severity level that the framework will log in the log output. You can and should set this to `200` (INFO) for production environments to filter out the DEBUG and the low-level DEFAULT entries.
461
+
462
+ AUDITING_LOG_USES_JSON
463
+ : JSON path `auditing.logUsesJSON`, type `boolean`, default `false`
464
+ : ENV variable `TI_AUDITING_LOG_USES_JSON`
465
+ : This setting controls whether the log entries would be sent to output formatted as JSONs or not. By default, the framework outputs log entries as prettified text. In some cases, however, you might want to have the entire entry as a JSON for further processing (for example, if you're sending all logs to Elasticsearch).
466
+
467
+ GCLOUD_API_KEY (Alpha)
468
+ : JSON path `gcloudIntegration.apiKey`
469
+ : This setting holds the API key for the GCloud integration module.
470
+
471
+ GCLOUD_PROJECT_ID (Alpha)
472
+ : JSON path `gcloudIntegration.projectID`
473
+ : This setting holds the project ID for the GCloud integration module.
474
+
475
+ LOCALIZATION_LABELS_PATH
476
+ : JSON path `localization.labelsPath`, type `Array<string>`, default `[]`
477
+ : ENV variable `TI_LOCALIZATION_LABELS_PATH`
478
+ : This setting holds a list of paths to custom `.json` files containing additional localization information. By default, the framework also provides such a file with English texts that can be customized further. All additional JSONs in these files have to follow the rules and structure of the `localization` module. The ENV variable currently supports providing only a single custom path.
479
+
480
+ LOCALIZATION_LANGUAGE
481
+ : JSON path `localization.language`, type `string`, default `en`
482
+ : ENV variable `TI_LOCALIZATION_LANGUAGE`
483
+ : This setting specifies the default framework language. It will be used when translating labels into a localized text.
484
+
485
+ MEMORY_CACHE_AUTH_KEY
486
+ : JSON path `memoryCache.authKey`, type `string`, default `undefined`
487
+ : ENV variable `TI_MEMORY_CACHE_AUTH_KEY`
488
+ : This setting holds the Redis password for accessing the Redis server if such a password is required.
489
+
490
+ MEMORY_CACHE_REDIS_DB
491
+ : JSON path `memoryCache.redisDB`, type `number`, default `0`
492
+ : ENV variable `TI_MEMORY_CACHE_REDIS_DB`
493
+ : This setting specifies the Redis DB to be used for all operations. When setting this make sure that the Redis server actually supports multiple DBs (for example, Redis Cloud has only one DB with ID `0`).
494
+
495
+ MEMORY_CACHE_REDIS_HOST
496
+ : JSON path `memoryCache.redisHost`, type `string`, default `127.0.0.1`
497
+ : ENV variable `TI_MEMORY_CACHE_REDIS_HOST`
498
+ : This setting holds the Redis server hostname. It can be an IP or URL depending on your configuration.
499
+
500
+ MEMORY_CACHE_REDIS_PORT
501
+ : JSON path `memoryCache.redisPort`, type `number`, default `6379`
502
+ : ENV variable `TI_MEMORY_CACHE_REDIS_PORT`
503
+ : This setting holds the Redis server port. By default, Redis uses `6379` however many implementations might use a custom port that needs to be specified in the connection settings.
504
+
505
+ MEMORY_CACHE_RETRY_MAX_ATTEMPTS
506
+ : JSON path `memoryCache.retryMaxAttempts`, type `number`, default `undefined`
507
+ : ENV variable `TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS`
508
+ : This setting holds the maximum number of attempts to (re)connect to the Redis server. By default, there is no limit.
509
+
510
+ MEMORY_CACHE_RETRY_MAX_INTERVAL
511
+ : JSON path `memoryCache.retryMaxInterval`, type `number`, default `1000`
512
+ : ENV variable `TI_MEMORY_CACHE_RETRY_MAX_INTERVAL`
513
+ : This setting holds the maximum interval in milliseconds between retries when the Redis server is not available.
514
+
515
+ MEMORY_CACHE_USER
516
+ : JSON path `memoryCache.user`, type `string`, default `default`
517
+ : ENV variable `TI_MEMORY_CACHE_USER`
518
+ : This setting holds the Redis username for accessing the Redis server if this is supported by the Redis version (it will be ignored otherwise).
519
+
520
+ MESSAGE_EXCHANGE_QUEUE_PREFIX (Advanced)
521
+ : JSON path `messageExchange.messageQueuePrefix`, type `string`, default `ti:messages:`
522
+ : This setting holds the Redis key prefix for the queues that will hold the messages of the message exchange. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
523
+
524
+ MESSAGE_EXCHANGE_MESSAGE_STORE (Advanced)
525
+ : JSON path `messageExchange.messageStore`, type `string`, default `ti:messages:store`
526
+ : This setting holds the Redis key name of the hash table that will hold the message payloads of the message exchange. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
527
+
528
+ MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED (Advanced)
529
+ : JSON path `messageExchange.securityHashEnabled`, type `boolean`, default `true`
530
+ : ENV variable `TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED`
531
+ : This setting controls whether the message exchange will use a control hash mechanism to ensure there is no tampering with the messages in between service calls. In most cases you would want to keep this enabled since it ensures the integrity of your data. If you are concerned about performance (the keyed `HMAC-SHA256` hash is rapid, but it still eats some milliseconds) you might want to try and disable this to see if it makes any notable difference.
532
+
533
+ MESSAGE_EXCHANGE_SECURITY_HASH_KEY (Advanced)
534
+ : JSON path `messageExchange.securityHashKey`, type `string`, default `empty (must be set)`
535
+ : ENV variable `TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY`
536
+ : This setting holds the HMAC key used by the message exchange control hash mechanism. By default it is empty, which leaves tamper protection ineffective and logs a startup warning. You absolutely must provide your own private key via the ENV variable in any real deployment. Depending on your configuration and infrastructure, it might come from a secure storage, HSM, key vault, etc.
537
+
538
+ MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME
539
+ : JSON path `messageExchange.traceExpirationTime`, type `number`, default `3600`
540
+ : This setting specifies the expiration time in seconds of the Redis key that will hold the message trace entries. Set this to `0` to disable expiration altogether.
541
+
542
+ MESSAGE_EXCHANGE_TRACE_LOG_ENABLED
543
+ : JSON path `messageExchange.traceLogEnabled`, type `boolean`, default `false`
544
+ : ENV variable `TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED`
545
+ : This setting controls whether the `auditing` module should output all trace messages as normal log entries or not. Normally, you don't want that since it will clutter the standard log quite a lot. All traces go to their own storage and can be reviewed and processed separately from the log entries. In some cases, however, as in debugging, enabling this can help you identify a hard to track problem.
546
+
547
+ MESSAGE_EXCHANGE_TRACE_REPOSITORY (Advanced)
548
+ : JSON path `messageExchange.traceRepository`, type `string`, default `ti:messages:trace`
549
+ : This setting holds the Redis key name for the message trace cache storage. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
550
+
551
+ SERVICE_EXECUTION_TIMEOUT
552
+ : JSON path `serviceConfig.executionTimeout`, type `number`, default `180000`
553
+ : This setting specifies the timeout in milliseconds of the service call executions at tier 2 of the architecture. Any service call that hasn't received response within this time will interrupt the wait and raise an `E_COM_SERVICE_EXEC_TIMEOUT` exception. Please keep in mind that reaching the timeout does not mean the remote service did not process the request. You might want to tweak this setting if you have many time-consuming operations in business services, or you plan to integrate with slow APIs.
554
+
555
+ SERVICE_HEALTH_CHECK_ADDRESS (Advanced)
556
+ : JSON path `serviceConfig.healthCheckAddress`, type `string`, default `ti:services:registry:health:`
557
+ : This setting specifies the address of the health check report endpoint for the microservice. In the default implementation this is a prefix for a Redis key that gets updated once at every `SERVICE_HEALTH_CHECK_INTERVAL`. If you override the `reportHealthy` method of the microservice, this setting can contain a URL or another type of destination that can be used by your custom implementation.
558
+
559
+ SERVICE_HEALTH_CHECK_INTERVAL (Advanced)
560
+ : JSON path `serviceConfig.healthCheckInterval`, type `CRON string`, default `*/1 * * * * *`
561
+ : This setting specifies the health check report interval at which the endpoint in `SERVICE_HEALTH_CHECK_ADDRESS` is notified.
562
+
563
+ SERVICE_HEALTH_CHECK_TIMEOUT (Advanced)
564
+ : JSON path `serviceConfig.healthCheckTimeout`, type `number`, default `3`
565
+ : This setting specifies the timeout in seconds after which a microservice is no longer considered healthy. In the default implementation this represents an expiration parameter to the Redis key defined in `SERVICE_HEALTH_CHECK_ADDRESS`. Essentially, if the microservice does not update the Redis key within this time interval, it will expire and the monitoring application will lose the healthy status of the microservice. If you override the `reportHealthy` method of the microservice, this setting can be used for your custom implementation as needed.
566
+
567
+ SERVICE_REGISTRY_ADDRESS (Advanced)
568
+ : JSON path `serviceConfig.serviceRegistryAddress`, type `string`, default `ti:services:registry:catalog:`
569
+ : This setting holds the prefix of the Redis key name used as a business service registry. If the microservice is a `ServiceProvider`, on start up it will register its business service portfolio in that Redis set. Also, on each service call that same registry will be searched for the existence of the called business service. This is not something you should modify unless you are making a customized implementation of the tier 1 architectural layer.
570
+
571
+ OPERATION_MODE
572
+ : JSON path `operationMode`, type `string`, default `production`
573
+ : ENV variable `NODE_ENV`
574
+ : This setting holds the current operation mode of the node application. It will inherit the value from the `NODE_ENV` variable if it exists, otherwise will use its default.
575
+
576
+ ## Advanced topics
577
+
578
+ ### Localization
579
+
580
+ The **ti-engine** framework provides a localization mechanism that allows you to translate labels into localized text. The framework comes with a default set of labels that can be found in the `localization` module. You can add your own custom labels to this set by providing one or more JSON files with the same structure as the default one (see below). The path to these files should be specified in the `LOCALIZATION_LABELS_PATH` setting. On startup, the framework will load all the JSON files and merge them into a single repository.
581
+
582
+ The following is an example of a custom localization file. The names in brackets can be replaced with your own values. The depth of the three is unlimited and can be used to create a hierarchy of labels.
583
+
584
+ ```json
585
+ {
586
+ "[category]": {
587
+ "[sub-category]": {
588
+ "[label]": {
589
+ "[language code]": "[localized label text]"
590
+ }
591
+ }
592
+ }
593
+ }
594
+ ```
595
+
596
+ The individual languages are specified with a two-letter code according to ISO 639-1. The default language is `en` (English). If you want to use a different language, you can set the `LOCALIZATION_LANGUAGE` setting to the desired language code.
597
+
549
598
  If you want to localize the system labels of the framework, you can do so by providing a custom localization file with the same structure as the default one but containing only the languages you want to add. For example, the default English label for `E_UNKNOWN_ERROR` is found in JSON path `system.exceptions.0.en`. To add an entry for German, your file needs to contain a `system.exceptions.0.de` property. The localization module will handle the rest. You can use the same approach to modify the existing English labels as well. The full list of system labels is found in the `bin/localization/labels.json` file. Avoid modifying the default file since it might get overwritten by future updates.