UNPKG

3.32 kBJavaScriptView Raw
1"use strict"
2
3const GET = "GET"
4const healthzUrl = "/healthz"
5
6class 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
148module.exports = Healthz