1 | "use strict"
|
2 |
|
3 | const GET = "GET"
|
4 | const healthzUrl = "/healthz"
|
5 |
|
6 | class Healthz {
|
7 | constructor(logger, app, options={}) {
|
8 | let checkInterval = 10000
|
9 | if (arguments.length === 2) {
|
10 | options = app
|
11 | app = null
|
12 | }
|
13 |
|
14 | checkInterval = options.checkInterval
|
15 |
|
16 | this.logger = logger
|
17 | this.watchers = []
|
18 | this.checkInterval = checkInterval
|
19 | this.state = {}
|
20 | this.app = app
|
21 | this.timer = null
|
22 | this.onStop = null
|
23 | }
|
24 |
|
25 | registerWatcher(name, watcherFn, {required, defaultIsOk}) {
|
26 | this.watchers.push({
|
27 | state: {},
|
28 | name, watcherFn,
|
29 | isOk: !!defaultIsOk,
|
30 | required: !!required
|
31 | })
|
32 | }
|
33 |
|
34 | async start(app=null) {
|
35 | if (app) {
|
36 | this.app = app
|
37 | }
|
38 |
|
39 | this._loop()
|
40 | }
|
41 |
|
42 | async stop() {
|
43 | return new Promise((resolve) => {
|
44 | this.onStop = () => {
|
45 | this.watchers.map((watcher) => watcher.isOk = false)
|
46 | resolve()
|
47 | }
|
48 |
|
49 | if (this.timer || this.timer === null) {
|
50 | clearTimeout(this.timer)
|
51 | this.onStop()
|
52 | }
|
53 | })
|
54 | }
|
55 |
|
56 | async whenOk() {
|
57 | if (this.status.isOk) return
|
58 | await this._waitForOk()
|
59 | }
|
60 |
|
61 | get handler() {
|
62 | return this._handler.bind(this)
|
63 | }
|
64 |
|
65 | get middleware() {
|
66 | return this._middleware.bind(this)
|
67 | }
|
68 |
|
69 | get status() {
|
70 | let sumIsOk = true
|
71 | let detailed = {}
|
72 |
|
73 | this.watchers.forEach(({name, isOk, required}) => {
|
74 | detailed[name] = isOk
|
75 | if (required && !isOk) {
|
76 | sumIsOk = false
|
77 | this.logger.error(`${name} not OK`)
|
78 | }
|
79 | })
|
80 |
|
81 | return {isOk: sumIsOk, detailed}
|
82 | }
|
83 |
|
84 | async _middleware(ctx, next) {
|
85 | let {method, url} = ctx.request
|
86 |
|
87 | if (method === GET && url === healthzUrl) {
|
88 | await this._handler(ctx)
|
89 | } else {
|
90 | await next()
|
91 | }
|
92 | }
|
93 |
|
94 | async _handler(ctx) {
|
95 | let {isOk, detailed} = this.status
|
96 |
|
97 | ctx.body = detailed
|
98 | ctx.status = isOk ? 200 : 500
|
99 | }
|
100 |
|
101 | async _waitForOk() {
|
102 | return this._promiseWaitForOk = this._promiseWaitForOk || new Promise(resolve => this._resolveOk = resolve).finally(() => {
|
103 | this._promiseWaitForOk = null
|
104 | this._resolveOk = () => {}
|
105 | })
|
106 | }
|
107 |
|
108 | async _loop() {
|
109 | this.logger.trace("Loop...")
|
110 | this.timer = null
|
111 | await this._executeWatchers()
|
112 |
|
113 | if (this._promiseWaitForOk && this.status.isOk) {
|
114 | this._resolveOk()
|
115 | }
|
116 |
|
117 | if (this.onStop) {
|
118 | this.logger.debug("Stopping...")
|
119 | this.onStop()
|
120 | } else {
|
121 | let interval = Math.round(this.checkInterval + Math.random() * this.checkInterval / 2)
|
122 | this.logger.trace(`Scheduling in ${interval}ms...`)
|
123 | this.timer = setTimeout(this._loop.bind(this), interval)
|
124 | }
|
125 | }
|
126 |
|
127 | async _executeWatchers() {
|
128 | return Promise.all(this.watchers.map((watcher) => {
|
129 | return new Promise(resolve => {
|
130 | if (watcher.running) {
|
131 | resolve(watcher.isOk = false)
|
132 | }
|
133 | const timeout = setTimeout(() => resolve(watcher.isOk = false), 1000)
|
134 | watcher.running = true
|
135 | watcher.watcherFn
|
136 | .call(watcher.state, this.logger, this.app)
|
137 | .then(isOk => {
|
138 | clearTimeout(timeout)
|
139 | resolve(watcher.isOk = isOk)
|
140 | })
|
141 | .catch(() => resolve(watcher.isOk = false))
|
142 | .finally(() => watcher.running = false)
|
143 | })
|
144 | }))
|
145 | }
|
146 | }
|
147 |
|
148 | module.exports = Healthz
|