Added /healthz JSON response for heartbeat data. #1940 (#1984)

This commit is contained in:
Jacob Goldman 2020-08-31 11:29:12 -04:00 committed by GitHub
parent de41646fc4
commit 75c8fdeed2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 46 additions and 7 deletions

32
src/node/app/health.ts Normal file
View File

@ -0,0 +1,32 @@
import * as http from "http"
import { HttpCode, HttpError } from "../../common/http"
import { HttpProvider, HttpResponse, Route, Heart, HttpProviderOptions } from "../http"
/**
* Check the heartbeat.
*/
export class HealthHttpProvider extends HttpProvider {
public constructor(options: HttpProviderOptions, private readonly heart: Heart) {
super(options)
}
public async handleRequest(route: Route, request: http.IncomingMessage): Promise<HttpResponse> {
if (!this.authenticated(request)) {
if (this.isRoot(route)) {
return { redirect: "/login", query: { to: route.fullPath } }
}
throw new HttpError("Unauthorized", HttpCode.Unauthorized)
}
const result = {
cache: false,
mime: "application/json",
content: {
status: this.heart.alive() ? "alive" : "expired",
lastHeartbeat: this.heart.lastHeartbeat,
},
}
return result
}
}

View File

@ -3,8 +3,9 @@ import * as cp from "child_process"
import { promises as fs } from "fs" import { promises as fs } from "fs"
import http from "http" import http from "http"
import * as path from "path" import * as path from "path"
import { CliMessage, OpenCommandPipeArgs } from "../../lib/vscode/src/vs/server/ipc" import { CliMessage } from "../../lib/vscode/src/vs/server/ipc"
import { plural } from "../common/util" import { plural } from "../common/util"
import { HealthHttpProvider } from "./app/health"
import { LoginHttpProvider } from "./app/login" import { LoginHttpProvider } from "./app/login"
import { ProxyHttpProvider } from "./app/proxy" import { ProxyHttpProvider } from "./app/proxy"
import { StaticHttpProvider } from "./app/static" import { StaticHttpProvider } from "./app/static"
@ -80,6 +81,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise<void>
httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/proxy", ProxyHttpProvider)
httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword)
httpServer.registerHttpProvider("/static", StaticHttpProvider) httpServer.registerHttpProvider("/static", StaticHttpProvider)
httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart)
await loadPlugins(httpServer, args) await loadPlugins(httpServer, args)

View File

@ -396,23 +396,26 @@ export abstract class HttpProvider {
export class Heart { export class Heart {
private heartbeatTimer?: NodeJS.Timeout private heartbeatTimer?: NodeJS.Timeout
private heartbeatInterval = 60000 private heartbeatInterval = 60000
private lastHeartbeat = 0 public lastHeartbeat = 0
public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {} public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise<boolean>) {}
public alive(): boolean {
const now = Date.now()
return now - this.lastHeartbeat < this.heartbeatInterval
}
/** /**
* Write to the heartbeat file if we haven't already done so within the * Write to the heartbeat file if we haven't already done so within the
* timeout and start or reset a timer that keeps running as long as there is * timeout and start or reset a timer that keeps running as long as there is
* activity. Failures are logged as warnings. * activity. Failures are logged as warnings.
*/ */
public beat(): void { public beat(): void {
const now = Date.now() if (!this.alive()) {
if (now - this.lastHeartbeat >= this.heartbeatInterval) {
logger.trace("heartbeat") logger.trace("heartbeat")
fs.outputFile(this.heartbeatPath, "").catch((error) => { fs.outputFile(this.heartbeatPath, "").catch((error) => {
logger.warn(error.message) logger.warn(error.message)
}) })
this.lastHeartbeat = now this.lastHeartbeat = Date.now()
if (typeof this.heartbeatTimer !== "undefined") { if (typeof this.heartbeatTimer !== "undefined") {
clearTimeout(this.heartbeatTimer) clearTimeout(this.heartbeatTimer)
} }
@ -457,7 +460,7 @@ export class HttpServer {
private listenPromise: Promise<string | null> | undefined private listenPromise: Promise<string | null> | undefined
public readonly protocol: "http" | "https" public readonly protocol: "http" | "https"
private readonly providers = new Map<string, HttpProvider>() private readonly providers = new Map<string, HttpProvider>()
private readonly heart: Heart public readonly heart: Heart
private readonly socketProvider = new SocketProxyProvider() private readonly socketProvider = new SocketProxyProvider()
/** /**
@ -602,8 +605,10 @@ export class HttpServer {
} }
private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => { private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise<void> => {
this.heart.beat()
const route = this.parseUrl(request) const route = this.parseUrl(request)
if (route.providerBase !== "/healthz") {
this.heart.beat()
}
const write = (payload: HttpResponse): void => { const write = (payload: HttpResponse): void => {
response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, {
"Content-Type": payload.mime || getMediaMime(payload.filePath), "Content-Type": payload.mime || getMediaMime(payload.filePath),