From 42485fbd036b46845525d88473ff6684e0a24fe1 Mon Sep 17 00:00:00 2001 From: Jacob Goldman Date: Tue, 18 Aug 2020 22:11:21 -0700 Subject: [PATCH 1/2] Added /healthz JSON response for heartbeat data. #1940 --- src/node/app/health.ts | 43 ++++++++++++++++++++++++++++++++++++++++++ src/node/entry.ts | 2 ++ src/node/http.ts | 9 +++++---- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/node/app/health.ts diff --git a/src/node/app/health.ts b/src/node/app/health.ts new file mode 100644 index 000000000000..bad524a179a7 --- /dev/null +++ b/src/node/app/health.ts @@ -0,0 +1,43 @@ +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) + } + + private alive(): Boolean { + const now = Date.now() + return (now - this.heart.lastHeartbeat < this.heart.heartbeatInterval) + } + + public async handleRequest(route: Route, request: http.IncomingMessage): Promise { + 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.alive()) ? 'alive' : 'expired', + lastHeartbeat: this.heart.lastHeartbeat + + } + } + + return result + + } +} diff --git a/src/node/entry.ts b/src/node/entry.ts index dffa32583b32..3831839386d4 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -7,6 +7,7 @@ import { ProxyHttpProvider } from "./app/proxy" import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" +import { HealthHttpProvider } from "./app/health" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" @@ -78,6 +79,7 @@ const main = async (args: Args, cliArgs: Args, configArgs: Args): Promise httpServer.registerHttpProvider("/proxy", ProxyHttpProvider) httpServer.registerHttpProvider("/login", LoginHttpProvider, args.config!, envPassword) httpServer.registerHttpProvider("/static", StaticHttpProvider) + httpServer.registerHttpProvider("/healthz", HealthHttpProvider, httpServer.heart) await loadPlugins(httpServer, args) diff --git a/src/node/http.ts b/src/node/http.ts index 5c8346f7c3ba..071753e46426 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -395,8 +395,8 @@ export abstract class HttpProvider { */ export class Heart { private heartbeatTimer?: NodeJS.Timeout - private heartbeatInterval = 60000 - private lastHeartbeat = 0 + public heartbeatInterval = 60000 + public lastHeartbeat = 0 public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} @@ -457,7 +457,7 @@ export class HttpServer { private listenPromise: Promise | undefined public readonly protocol: "http" | "https" private readonly providers = new Map() - private readonly heart: Heart + public readonly heart: Heart private readonly socketProvider = new SocketProxyProvider() /** @@ -602,8 +602,9 @@ export class HttpServer { } private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise => { - this.heart.beat() const route = this.parseUrl(request) + if (route.providerBase !== '/healthz') + this.heart.beat() const write = (payload: HttpResponse): void => { response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { "Content-Type": payload.mime || getMediaMime(payload.filePath), From 54ddd8bbf875b9964652470f3a83fc05695e9851 Mon Sep 17 00:00:00 2001 From: Jacob Goldman Date: Mon, 31 Aug 2020 07:37:03 -0700 Subject: [PATCH 2/2] linter changes and refactored alive() to Heart --- src/node/app/health.ts | 47 ++++++++++++++++-------------------------- src/node/entry.ts | 4 ++-- src/node/http.ts | 16 ++++++++------ 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/src/node/app/health.ts b/src/node/app/health.ts index bad524a179a7..6a3aae94c730 100644 --- a/src/node/app/health.ts +++ b/src/node/app/health.ts @@ -6,38 +6,27 @@ import { HttpProvider, HttpResponse, Route, Heart, HttpProviderOptions } from ". * Check the heartbeat. */ export class HealthHttpProvider extends HttpProvider { + public constructor(options: HttpProviderOptions, private readonly heart: Heart) { + super(options) + } - public constructor( - options: HttpProviderOptions, - private readonly heart: Heart - ) { - super(options) + public async handleRequest(route: Route, request: http.IncomingMessage): Promise { + if (!this.authenticated(request)) { + if (this.isRoot(route)) { + return { redirect: "/login", query: { to: route.fullPath } } + } + throw new HttpError("Unauthorized", HttpCode.Unauthorized) } - private alive(): Boolean { - const now = Date.now() - return (now - this.heart.lastHeartbeat < this.heart.heartbeatInterval) + const result = { + cache: false, + mime: "application/json", + content: { + status: this.heart.alive() ? "alive" : "expired", + lastHeartbeat: this.heart.lastHeartbeat, + }, } - public async handleRequest(route: Route, request: http.IncomingMessage): Promise { - 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.alive()) ? 'alive' : 'expired', - lastHeartbeat: this.heart.lastHeartbeat - - } - } - - return result - - } + return result + } } diff --git a/src/node/entry.ts b/src/node/entry.ts index 3831839386d4..a5fbcc4483c2 100644 --- a/src/node/entry.ts +++ b/src/node/entry.ts @@ -2,18 +2,18 @@ import { field, logger } from "@coder/logger" import * as cp from "child_process" import * as path from "path" import { CliMessage } from "../../lib/vscode/src/vs/server/ipc" +import { plural } from "../common/util" +import { HealthHttpProvider } from "./app/health" import { LoginHttpProvider } from "./app/login" import { ProxyHttpProvider } from "./app/proxy" import { StaticHttpProvider } from "./app/static" import { UpdateHttpProvider } from "./app/update" import { VscodeHttpProvider } from "./app/vscode" -import { HealthHttpProvider } from "./app/health" import { Args, bindAddrFromAllSources, optionDescriptions, parse, readConfigFile, setDefaults } from "./cli" import { AuthType, HttpServer, HttpServerOptions } from "./http" import { loadPlugins } from "./plugin" import { generateCertificate, hash, humanPath, open } from "./util" import { ipcMain, wrap } from "./wrapper" -import { plural } from "../common/util" process.on("uncaughtException", (error) => { logger.error(`Uncaught exception: ${error.message}`) diff --git a/src/node/http.ts b/src/node/http.ts index 071753e46426..37bbcfbdd0dc 100644 --- a/src/node/http.ts +++ b/src/node/http.ts @@ -395,24 +395,27 @@ export abstract class HttpProvider { */ export class Heart { private heartbeatTimer?: NodeJS.Timeout - public heartbeatInterval = 60000 + private heartbeatInterval = 60000 public lastHeartbeat = 0 public constructor(private readonly heartbeatPath: string, private readonly isActive: () => Promise) {} + 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 * timeout and start or reset a timer that keeps running as long as there is * activity. Failures are logged as warnings. */ public beat(): void { - const now = Date.now() - if (now - this.lastHeartbeat >= this.heartbeatInterval) { + if (!this.alive()) { logger.trace("heartbeat") fs.outputFile(this.heartbeatPath, "").catch((error) => { logger.warn(error.message) }) - this.lastHeartbeat = now + this.lastHeartbeat = Date.now() if (typeof this.heartbeatTimer !== "undefined") { clearTimeout(this.heartbeatTimer) } @@ -603,8 +606,9 @@ export class HttpServer { private onRequest = async (request: http.IncomingMessage, response: http.ServerResponse): Promise => { const route = this.parseUrl(request) - if (route.providerBase !== '/healthz') - this.heart.beat() + if (route.providerBase !== "/healthz") { + this.heart.beat() + } const write = (payload: HttpResponse): void => { response.writeHead(payload.redirect ? HttpCode.Redirect : payload.code || HttpCode.Ok, { "Content-Type": payload.mime || getMediaMime(payload.filePath),