1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const Decorators_1 = require("../ioc/Decorators");
|
4 | const LogManager_1 = require("../log/LogManager");
|
5 | const NewrelicUtil_1 = require("../newrelic/NewrelicUtil");
|
6 | const LOGGER = LogManager_1.LogManager.getLogger(__filename);
|
7 | exports.HEALTH_CHECK_GROUP = 'inceptum:health';
|
8 | function RegisterAsHealthCheck(target) {
|
9 | Decorators_1.RegisterInGroup(exports.HEALTH_CHECK_GROUP)(target);
|
10 | Decorators_1.Lazy(false)(target);
|
11 | }
|
12 | exports.RegisterAsHealthCheck = RegisterAsHealthCheck;
|
13 | var HealthCheckStatus;
|
14 | (function (HealthCheckStatus) {
|
15 | HealthCheckStatus["OK"] = "OK";
|
16 | HealthCheckStatus["NOT_READY"] = "NOT_READY";
|
17 | HealthCheckStatus["WARNING"] = "WARNING";
|
18 | HealthCheckStatus["CRITICAL"] = "CRITICAL";
|
19 | HealthCheckStatus["STOPPING"] = "STOPPING";
|
20 | })(HealthCheckStatus = exports.HealthCheckStatus || (exports.HealthCheckStatus = {}));
|
21 | var HealthCheckType;
|
22 | (function (HealthCheckType) {
|
23 | HealthCheckType[HealthCheckType["CONTEXT"] = 0] = "CONTEXT";
|
24 | HealthCheckType[HealthCheckType["DEPENDENCY"] = 1] = "DEPENDENCY";
|
25 | HealthCheckType[HealthCheckType["GROUP"] = 2] = "GROUP";
|
26 | })(HealthCheckType = exports.HealthCheckType || (exports.HealthCheckType = {}));
|
27 | function getStatusIndex(status) {
|
28 | return [HealthCheckStatus.OK, HealthCheckStatus.NOT_READY, HealthCheckStatus.WARNING, HealthCheckStatus.CRITICAL, HealthCheckStatus.STOPPING].indexOf(status);
|
29 | }
|
30 | class HealthCheckResult {
|
31 | constructor(status, message, timestamp, data) {
|
32 | this.status = status;
|
33 | this.message = message;
|
34 | this.data = data;
|
35 | this.timestamp = timestamp || new Date().getTime();
|
36 | }
|
37 | static getInitialResult() {
|
38 | return new HealthCheckResult(HealthCheckStatus.NOT_READY, 'First check not run yet');
|
39 | }
|
40 | static staleResult(healthCheck, lastResult) {
|
41 | return new HealthCheckResult(HealthCheckStatus.CRITICAL, `Stale check result for check ${healthCheck.checkName}. Last result on ${lastResult.timestamp}`);
|
42 | }
|
43 | }
|
44 | exports.HealthCheckResult = HealthCheckResult;
|
45 | class HealthCheck {
|
46 | constructor(checkName, sleepMillis = 60000, staleNumFails = 2, runImmediately = false) {
|
47 | this.checkName = checkName;
|
48 | this.sleepMillis = sleepMillis;
|
49 | this.staleNumFails = staleNumFails;
|
50 | this.runImmediately = runImmediately;
|
51 | this.started = false;
|
52 | this.lastResult = HealthCheckResult.getInitialResult();
|
53 | this.checkRunning = false;
|
54 | }
|
55 | getCheckName() {
|
56 | return this.checkName;
|
57 | }
|
58 | getSleepMillis() {
|
59 | return this.sleepMillis;
|
60 | }
|
61 | start() {
|
62 | if (this.started) {
|
63 | return;
|
64 | }
|
65 | this.started = true;
|
66 | this.doStart();
|
67 | if (this.runImmediately) {
|
68 | this.runCheck();
|
69 | }
|
70 | }
|
71 | async runCheck() {
|
72 | if (!this.checkRunning) {
|
73 | this.checkRunning = true;
|
74 | try {
|
75 | this.lastResult = await this.doCheck();
|
76 | }
|
77 | catch (e) {
|
78 | this.lastResult = new HealthCheckResult(HealthCheckStatus.CRITICAL, `There was an error running check ${this.getCheckName()}: ${e.message}`);
|
79 | }
|
80 | if (this.lastResult.status === HealthCheckStatus.CRITICAL) {
|
81 | LOGGER.error(`Health check ${this.getCheckName()} failed with status CRITICAL`, { status: this.lastResult.status, message: this.lastResult.message, data: this.lastResult.data });
|
82 | }
|
83 | this.checkRunning = false;
|
84 | }
|
85 | }
|
86 | doStart() {
|
87 | this.timer = setInterval(async () => { await this.runCheck(); }, this.getSleepMillis());
|
88 | }
|
89 | stop() {
|
90 | if (this.started) {
|
91 | this.doStop();
|
92 | }
|
93 | }
|
94 | doStop() {
|
95 | if (this.timer) {
|
96 | clearInterval(this.timer);
|
97 | }
|
98 | }
|
99 | isStarted() {
|
100 | return this.started;
|
101 | }
|
102 | getLastResult() {
|
103 | const threshold = (new Date().getTime()) - this.getSleepMillis() * this.staleNumFails;
|
104 | if (this.lastResult.timestamp < threshold) {
|
105 | return HealthCheckResult.staleResult(this, this.lastResult);
|
106 | }
|
107 | return this.lastResult;
|
108 | }
|
109 | }
|
110 | exports.HealthCheck = HealthCheck;
|
111 | class HealthCheckGroup extends HealthCheck {
|
112 | constructor(groupName) {
|
113 | super(`Group: ${groupName}`, 0, 0);
|
114 | this.healthChecks = new Map();
|
115 | this.groupKey = groupName;
|
116 | }
|
117 | addCheck(healthCheck, asName) {
|
118 | this.addCheckAs(healthCheck, asName || healthCheck.getCheckName());
|
119 | }
|
120 | addCheckAs(healthCheck, asName) {
|
121 | if (asName.indexOf('.') >= 0) {
|
122 | const parts = asName.split('.', 2);
|
123 | const firstPart = parts[0];
|
124 | const remainderPart = asName.substr(firstPart.length + 1);
|
125 | const group = this.healthChecks.get(firstPart);
|
126 | if (group && (group instanceof HealthCheckGroup)) {
|
127 | group.addCheckAs(healthCheck, remainderPart);
|
128 | }
|
129 | else if (group) {
|
130 | throw new Error(`A health check with name ${firstPart} is already defined in this group`);
|
131 | }
|
132 | else {
|
133 | const newGroup = new HealthCheckGroup(firstPart);
|
134 | newGroup.addCheckAs(healthCheck, remainderPart);
|
135 | this.addCheck(newGroup, firstPart);
|
136 | }
|
137 | }
|
138 | else {
|
139 | this.healthChecks.set(asName, healthCheck);
|
140 | if (this.isStarted()) {
|
141 | healthCheck.start();
|
142 | }
|
143 | }
|
144 | }
|
145 | doStart() {
|
146 | this.healthChecks.forEach((healthCheck) => {
|
147 | healthCheck.start();
|
148 | });
|
149 | }
|
150 | doStop() {
|
151 | this.healthChecks.forEach((healthCheck) => {
|
152 | healthCheck.stop();
|
153 | });
|
154 | }
|
155 | async doCheck() {
|
156 | throw new Error('This shouldn\'t be called. We override getLastResult to get up-to-date info');
|
157 | }
|
158 | getLastResult() {
|
159 | const resp = new HealthCheckResult(HealthCheckStatus.OK, 'OK', new Date().getTime(), {});
|
160 | this.healthChecks.forEach((healthCheck, key) => {
|
161 |
|
162 | const lastResult = healthCheck.getLastResult();
|
163 | resp.data[key] = lastResult;
|
164 |
|
165 | if (this.ignoreHealthCheckStatus(healthCheck, lastResult.status)) {
|
166 | NewrelicUtil_1.NewrelicUtil.noticeError(new Error(lastResult.message), lastResult);
|
167 | }
|
168 | else {
|
169 | if (getStatusIndex(lastResult.status) > getStatusIndex(resp.status)) {
|
170 | resp.status = lastResult.status;
|
171 | resp.message = `Check ${healthCheck.getCheckName()} returned ${resp.status}`;
|
172 | }
|
173 | }
|
174 | });
|
175 | return resp;
|
176 | }
|
177 | |
178 |
|
179 |
|
180 |
|
181 | ignoreHealthCheckStatus(hc, status) {
|
182 | return status === HealthCheckStatus.CRITICAL && hc.getType() === HealthCheckType.DEPENDENCY;
|
183 | }
|
184 | getType() {
|
185 | return HealthCheckType.GROUP;
|
186 | }
|
187 | }
|
188 | exports.HealthCheckGroup = HealthCheckGroup;
|
189 |
|
\ | No newline at end of file |