Skip to content

Commit

Permalink
Add latency to html
Browse files Browse the repository at this point in the history
include latency status to the result also, differentiating
between 3 latency levels - low, med, high. Levels are
configurable for each check.

Related: #4
  • Loading branch information
smoliji committed Jul 1, 2024
1 parent 91abfef commit 837e935
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 7 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## [Unreleased]

### Added

- Latency status
- Latency to the HTML view

### [2.0.1] 2024-05-31

### Added
Expand Down
Binary file modified html-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 31 additions & 2 deletions src/healthz.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test, { describe } from 'node:test'
import { Status, createConfig, check } from './healthz'
import { equal } from 'node:assert'
import { Status, createConfig, check, LatencyStatus } from './healthz'
import { deepEqual, equal } from 'node:assert'

describe('healthz', () => {
test('Health check results are masked by default', async () => {
Expand Down Expand Up @@ -53,4 +53,33 @@ describe('healthz', () => {
})
equal(result.status, Status.Healthy)
})
test('Latency status thresholds can be set and are 100 and 500 by default', async () => {
deepEqual(
createConfig({ checks: [{ fn: async () => '', id: 'test' }] }).checks[0]
.latencyLevels,
[100, 500],
)
const result = await check({
checks: [
{
id: 'check',
fn: () => new Promise((resolve) => setTimeout(resolve, 10)),
latencyLevents: [20, 120],
},
{
id: 'check',
fn: () => new Promise((resolve) => setTimeout(resolve, 100)),
latencyLevents: [20, 120],
},
{
id: 'check',
fn: () => new Promise((resolve) => setTimeout(resolve, 200)),
latencyLevents: [20, 120],
},
],
})
equal(result.checks[0].latencyStatus, LatencyStatus.Low)
equal(result.checks[1].latencyStatus, LatencyStatus.Medium)
equal(result.checks[2].latencyStatus, LatencyStatus.High)
})
})
23 changes: 23 additions & 0 deletions src/healthz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Config {
required: boolean
fn: () => Promise<unknown>
maskOutput: boolean
latencyLevels: [number, number]
}>
timeout: number
}
Expand All @@ -36,6 +37,7 @@ export interface CheckResult {
* Milliseconds it took the check to finish. Wall clock time.
*/
latency: number
latencyStatus: LatencyStatus
}

export interface Option {
Expand Down Expand Up @@ -64,6 +66,13 @@ export interface Option {
* @default true
*/
maskOutput?: boolean
/**
* Latency levels for Low, Medium and High latency.
* It is expected that [0] < [1]. For example `latency < [0]` -> Low,
* `latency >= [0] && latency < [1]` -> medium, everything else is High
* @default [100, 500]
*/
latencyLevents?: [number, number]
}>
/**
* How many milliseconds to wait for the check to complete. It if fails
Expand All @@ -81,6 +90,7 @@ export function createConfig(option?: Option): Config {
required: x.required ?? false,
fn: x.fn,
maskOutput: x.maskOutput ?? true,
latencyLevels: x.latencyLevents ?? [100, 500],
})) ?? [],
timeout: option?.timeout ?? 5_000,
}
Expand All @@ -93,6 +103,12 @@ export function check(option?: Option) {

type RawCheckResult = Pick<CheckResult, 'latency' | 'rawOutput' | 'status'>

export enum LatencyStatus {
Low,
Medium,
High,
}

async function checkForConfig(config: Config): Promise<Result> {
const t0 = Date.now()
const timeout = createTimeout(config.timeout)
Expand Down Expand Up @@ -129,10 +145,17 @@ async function checkForConfig(config: Config): Promise<Result> {
rawOutput: results[i].rawOutput,
latency: results[i].latency,
required: x.required,
latencyStatus: latencyStatus(x.latencyLevels, results[i].latency)
})),
}
}

function latencyStatus(levels: [number, number], latency: number): LatencyStatus {
if (latency < levels[0]) return LatencyStatus.Low
if (latency < levels[1]) return LatencyStatus.Medium
return LatencyStatus.High
}

function createTimeout(ms: number) {
let ref: NodeJS.Timeout
return {
Expand Down
4 changes: 3 additions & 1 deletion src/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test, { describe } from 'node:test'
import { htmlResult, jsonResult, resultStatusCode } from './http'
import { CheckStatus, Status } from './healthz'
import { CheckStatus, LatencyStatus, Status } from './healthz'
import { deepEqual, equal } from 'node:assert'

describe('http', () => {
Expand Down Expand Up @@ -31,6 +31,7 @@ describe('http', () => {
rawOutput: 'ro',
required: true,
status: CheckStatus.Ok,
latencyStatus: LatencyStatus.Medium,
},
],
status: Status.Healthy,
Expand Down Expand Up @@ -59,6 +60,7 @@ describe('http', () => {
rawOutput: 'ro',
required: true,
status: CheckStatus.Ok,
latencyStatus: LatencyStatus.Medium,
},
],
status: Status.Healthy,
Expand Down
32 changes: 28 additions & 4 deletions src/http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CheckStatus, Result, Status } from './healthz'
import { CheckStatus, LatencyStatus, Result, Status } from './healthz'

export function jsonResult(result: Result) {
return {
Expand Down Expand Up @@ -45,7 +45,16 @@ export function htmlResult(result: Result) {
</div>
</li>
${result.checks
.map(x => html('service', { title: x.id, required: x.required, status: x.status, output: String(x.output ?? '') }))
.map((x) =>
html('service', {
title: x.id,
required: x.required,
status: x.status,
output: String(x.output ?? ''),
latencyStatus: x.latencyStatus,
latencyMs: x.latency,
}),
)
.join('\n')}
</ul>
</div>
Expand All @@ -55,7 +64,7 @@ export function htmlResult(result: Result) {
`
}

function html(piece: 'healthy' | 'unhealthy' | 'service' | 'check-ok' | 'check-error' | 'check-timeout', arg?: { title?: string, required?: boolean, status?: CheckStatus, output?: string }): string {
function html(piece: 'healthy' | 'unhealthy' | 'service' | 'check-ok' | 'check-error' | 'check-timeout' | 'latency' | 'latency-low' | 'latency-medium' | 'latency-high', arg?: { latencyMs?: number, latencyStatus?: LatencyStatus, title?: string, required?: boolean, status?: CheckStatus, output?: string }): string {
switch (piece) {
case 'healthy':
return '<i class="bi bi-check-circle-fill text-success " style="font-size: 3em;"></i>'
Expand All @@ -67,14 +76,29 @@ function html(piece: 'healthy' | 'unhealthy' | 'service' | 'check-ok' | 'check-e
return `<i class="bi bi-x-circle-fill text-danger" title="${arg?.output ?? ''}"></i>`
case 'check-timeout':
return `<i class="bi bi-clock-fill text-secondary" title="${arg?.output ?? ''}"></i>`
case 'latency':
if (arg?.latencyStatus === LatencyStatus.Low)
return html('latency-low', arg)
if (arg?.latencyStatus === LatencyStatus.Medium)
return html('latency-medium', arg)
return html('latency-high', arg)
case 'latency-low':
return `<span class="badge rounded-pill text-bg-success">${arg?.latencyMs ?? '??'} ms <i class="bi bi-reception-4"></i></span>`
case 'latency-medium':
return `<span class="badge rounded-pill text-bg-warning">${arg?.latencyMs ?? '??'} ms <i class="bi bi-reception-2"></i></span>`
case 'latency-high':
return `<span class="badge rounded-pill text-bg-danger">${arg?.latencyMs ?? '??'} ms <i class="bi bi-reception-1"></i></span>`
case 'service':
return `<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<span class="fw-medium">${arg?.title ?? 'Unknown'}</span>
<br>
<span class="fw-lighter">${arg?.required ? 'Required' : 'Optional'}</span>
</div>
<span>${arg?.status === CheckStatus.Ok ? html('check-ok') : arg?.status === CheckStatus.Error ? html('check-error') : html('check-timeout')}</span>
<div>
${html('latency', arg)}
<span>${arg?.status === CheckStatus.Ok ? html('check-ok') : arg?.status === CheckStatus.Error ? html('check-error') : html('check-timeout')}</span>
</div>
</li>
`
default:
Expand Down

0 comments on commit 837e935

Please sign in to comment.