@tsed/terminus 8.0.1 → 8.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -11
- package/src/TerminusModule.spec.ts +91 -0
- package/src/TerminusModule.ts +146 -0
- package/src/decorators/health.ts +28 -0
- package/src/index.ts +7 -0
- package/src/interfaces/TerminusSettings.ts +6 -0
- package/src/interfaces/interfaces.ts +9 -0
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@tsed/terminus",
|
|
3
3
|
"description": "Adds graceful shutdown and Kubernetes readiness / liveness checks for any HTTP applications.",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.3",
|
|
6
6
|
"author": "Romain Lenzotti",
|
|
7
7
|
"source": "./src/index.ts",
|
|
8
8
|
"main": "./lib/esm/index.js",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"typings": "./lib/types/index.d.ts",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
+
"@tsed/source": "./src/index.ts",
|
|
13
14
|
"types": "./lib/types/index.d.ts",
|
|
14
15
|
"import": "./lib/esm/index.js",
|
|
15
16
|
"default": "./lib/esm/index.js"
|
|
@@ -28,22 +29,22 @@
|
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@godaddy/terminus": "^4.12.1",
|
|
31
|
-
"@tsed/barrels": "8.0.
|
|
32
|
-
"@tsed/core": "8.0.
|
|
33
|
-
"@tsed/di": "8.0.
|
|
34
|
-
"@tsed/platform-http": "8.0.
|
|
35
|
-
"@tsed/schema": "8.0.
|
|
36
|
-
"@tsed/typescript": "8.0.
|
|
32
|
+
"@tsed/barrels": "8.0.3",
|
|
33
|
+
"@tsed/core": "8.0.3",
|
|
34
|
+
"@tsed/di": "8.0.3",
|
|
35
|
+
"@tsed/platform-http": "8.0.3",
|
|
36
|
+
"@tsed/schema": "8.0.3",
|
|
37
|
+
"@tsed/typescript": "8.0.3",
|
|
37
38
|
"eslint": "9.12.0",
|
|
38
39
|
"typescript": "5.4.5",
|
|
39
40
|
"vitest": "2.1.2"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@godaddy/terminus": "^4.7.1",
|
|
43
|
-
"@tsed/core": "8.0.
|
|
44
|
-
"@tsed/di": "8.0.
|
|
45
|
-
"@tsed/platform-http": "8.0.
|
|
46
|
-
"@tsed/schema": "8.0.
|
|
44
|
+
"@tsed/core": "8.0.3",
|
|
45
|
+
"@tsed/di": "8.0.3",
|
|
46
|
+
"@tsed/platform-http": "8.0.3",
|
|
47
|
+
"@tsed/schema": "8.0.3"
|
|
47
48
|
},
|
|
48
49
|
"peerDependenciesMeta": {
|
|
49
50
|
"@godaddy/terminus": {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {Injectable} from "@tsed/di";
|
|
2
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
3
|
+
|
|
4
|
+
import {Health} from "./decorators/health.js";
|
|
5
|
+
import {TerminusModule} from "./TerminusModule.js";
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
class MyService {
|
|
9
|
+
@Health("mongo")
|
|
10
|
+
mongo() {
|
|
11
|
+
return Promise.resolve("OK");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Health("/redis/health")
|
|
15
|
+
redis() {
|
|
16
|
+
return Promise.resolve("OK");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
$beforeShutdown() {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("TerminusModule", () => {
|
|
23
|
+
beforeEach(() =>
|
|
24
|
+
PlatformTest.create({
|
|
25
|
+
terminus: {
|
|
26
|
+
path: "/health"
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
afterEach(() => PlatformTest.reset());
|
|
31
|
+
|
|
32
|
+
it("should load health providers", async () => {
|
|
33
|
+
const terminusModule = PlatformTest.get<TerminusModule>(TerminusModule);
|
|
34
|
+
|
|
35
|
+
const {logger, ...props} = terminusModule.getConfiguration();
|
|
36
|
+
|
|
37
|
+
expect(props).toEqual({
|
|
38
|
+
beforeShutdown: expect.any(Function),
|
|
39
|
+
healthChecks: {
|
|
40
|
+
"/mongo/health": expect.any(Function),
|
|
41
|
+
"/redis/health": expect.any(Function),
|
|
42
|
+
"/health": expect.any(Function)
|
|
43
|
+
},
|
|
44
|
+
onSendFailureDuringShutdown: expect.any(Function),
|
|
45
|
+
onShutdown: expect.any(Function),
|
|
46
|
+
onSignal: expect.any(Function)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const result = await props.healthChecks["/health"]({});
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual([
|
|
52
|
+
{mongo: "OK"},
|
|
53
|
+
{"/redis/health": "OK"} // legacy
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
logger("event", {message: "message"});
|
|
57
|
+
|
|
58
|
+
expect(await terminusModule.$logRoutes([])).toEqual([
|
|
59
|
+
{
|
|
60
|
+
method: "GET",
|
|
61
|
+
name: "TerminusModule.dispatch()",
|
|
62
|
+
url: "/health"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
method: "GET",
|
|
66
|
+
name: "MyService.mongo()",
|
|
67
|
+
url: "/mongo/health"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
method: "GET",
|
|
71
|
+
name: "MyService.redis()",
|
|
72
|
+
url: "/redis/health"
|
|
73
|
+
}
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
await props.onSignal();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should emit event", async () => {
|
|
80
|
+
const terminusModule = PlatformTest.get<TerminusModule>(TerminusModule);
|
|
81
|
+
const service = PlatformTest.get<MyService>(MyService);
|
|
82
|
+
|
|
83
|
+
vi.spyOn(service, "$beforeShutdown");
|
|
84
|
+
|
|
85
|
+
const {beforeShutdown} = terminusModule.getConfiguration();
|
|
86
|
+
|
|
87
|
+
await beforeShutdown();
|
|
88
|
+
|
|
89
|
+
expect(service.$beforeShutdown).toHaveBeenCalledWith();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {createTerminus} from "@godaddy/terminus";
|
|
2
|
+
import {Constant, Inject, InjectorService, Module, OnInit, Provider} from "@tsed/di";
|
|
3
|
+
import type {PlatformRouteDetails} from "@tsed/platform-http";
|
|
4
|
+
import {concatPath} from "@tsed/schema";
|
|
5
|
+
import Http from "http";
|
|
6
|
+
import Https from "https";
|
|
7
|
+
|
|
8
|
+
import {TerminusSettings} from "./interfaces/TerminusSettings.js";
|
|
9
|
+
|
|
10
|
+
@Module()
|
|
11
|
+
export class TerminusModule implements OnInit {
|
|
12
|
+
@Constant("terminus", {})
|
|
13
|
+
private settings: TerminusSettings;
|
|
14
|
+
|
|
15
|
+
@Constant("terminus.path", "/health")
|
|
16
|
+
private basePath: string;
|
|
17
|
+
|
|
18
|
+
@Inject()
|
|
19
|
+
private injector: InjectorService;
|
|
20
|
+
|
|
21
|
+
@Inject(Http.Server)
|
|
22
|
+
private httpServer: Http.Server | null;
|
|
23
|
+
|
|
24
|
+
@Inject(Https.Server)
|
|
25
|
+
private httpsServer: Https.Server | null;
|
|
26
|
+
|
|
27
|
+
public $onInit() {
|
|
28
|
+
this.mount();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getConfiguration() {
|
|
32
|
+
const {path, ...props} = this.settings;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
logger: (event: string, error: any) =>
|
|
36
|
+
this.injector.logger.info({
|
|
37
|
+
event: event.toUpperCase(),
|
|
38
|
+
error_message: error.message
|
|
39
|
+
}),
|
|
40
|
+
healthChecks: this.getHealths(),
|
|
41
|
+
onSignal: this.createEmitter("$onSignal"),
|
|
42
|
+
onShutdown: this.createEmitter("$onShutdown"),
|
|
43
|
+
beforeShutdown: this.createEmitter("$beforeShutdown"),
|
|
44
|
+
onSendFailureDuringShutdown: this.createEmitter("$onSendFailureDuringShutdown"),
|
|
45
|
+
...props
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$logRoutes(routes: PlatformRouteDetails[]): Promise<PlatformRouteDetails[]> {
|
|
50
|
+
return Promise.resolve([
|
|
51
|
+
...routes,
|
|
52
|
+
{
|
|
53
|
+
url: this.basePath,
|
|
54
|
+
method: "GET",
|
|
55
|
+
name: `TerminusModule.dispatch()`
|
|
56
|
+
},
|
|
57
|
+
...this.getAll<{name: string}>("health").map(({provider, propertyKey, options}) => {
|
|
58
|
+
const path = this.getPath(provider, options.name);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
method: "GET",
|
|
62
|
+
name: `${provider.className}.${propertyKey}()`,
|
|
63
|
+
url: path
|
|
64
|
+
} as any;
|
|
65
|
+
})
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private mount() {
|
|
70
|
+
const terminusConfig = this.getConfiguration();
|
|
71
|
+
|
|
72
|
+
if (this.httpServer) {
|
|
73
|
+
createTerminus(this.httpServer, terminusConfig);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.httpsServer) {
|
|
77
|
+
createTerminus(this.httpsServer, terminusConfig);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private getAll<Opts = any>(
|
|
82
|
+
name: string
|
|
83
|
+
): {
|
|
84
|
+
provider: Provider;
|
|
85
|
+
propertyKey: string;
|
|
86
|
+
options: Opts;
|
|
87
|
+
}[] {
|
|
88
|
+
return this.injector.getProviders().flatMap((provider) => {
|
|
89
|
+
const metadata = provider.store.get(`terminus:${name}`);
|
|
90
|
+
|
|
91
|
+
if (metadata) {
|
|
92
|
+
return Object.entries(metadata).map(([propertyKey, options]: [string, Opts]) => {
|
|
93
|
+
return {
|
|
94
|
+
provider,
|
|
95
|
+
propertyKey,
|
|
96
|
+
options
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private getHealths() {
|
|
105
|
+
const subHealths = this.getAll<{name: string}>("health").reduce(
|
|
106
|
+
(healths, {provider, propertyKey, options: {name}}) => {
|
|
107
|
+
const instance = this.injector.get<any>(provider.token)!;
|
|
108
|
+
const callback = async (...args: any[]) => {
|
|
109
|
+
const result = await instance[propertyKey](...args);
|
|
110
|
+
|
|
111
|
+
return {[name]: result};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
let path = this.getPath(provider, name);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...healths,
|
|
118
|
+
[path]: callback
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
{} as Record<string, (state: any) => Promise<any>>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const healths: Record<string, any> = {
|
|
125
|
+
...subHealths,
|
|
126
|
+
[this.basePath]: (state: any) => {
|
|
127
|
+
const promises = Object.entries(subHealths).map(([path, callback]) => callback(state));
|
|
128
|
+
|
|
129
|
+
return Promise.all(promises);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return healths;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private getPath(provider: Provider<any>, name: string) {
|
|
137
|
+
let path = concatPath(provider.path, name);
|
|
138
|
+
return path.includes("health") ? path : concatPath(path, this.basePath);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private createEmitter(name: string) {
|
|
142
|
+
return (...args: any[]) => {
|
|
143
|
+
return this.injector.emit(name, ...args);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {Store} from "@tsed/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Create a readiness / liveness checks.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { Health } from "@tsed/terminus";
|
|
8
|
+
*
|
|
9
|
+
* @Controller("/mongo")
|
|
10
|
+
* class MongoCtrl {
|
|
11
|
+
* @Health("/health")
|
|
12
|
+
* health() {
|
|
13
|
+
* // Here check the mongo health
|
|
14
|
+
* return Promise.resolve();
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* @param name
|
|
19
|
+
* @decorator
|
|
20
|
+
* @terminus
|
|
21
|
+
*/
|
|
22
|
+
export function Health(name: string): MethodDecorator {
|
|
23
|
+
return <Function>(target: Object, propertyKey: string) => {
|
|
24
|
+
Store.from(target).merge(`terminus:health`, {
|
|
25
|
+
[propertyKey]: {name}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
package/src/index.ts
ADDED