Add healthcheck for client

This commit is contained in:
Andras Schmelczer 2025-11-18 21:49:35 +00:00
parent 418e09f08a
commit c92aa63d71
5 changed files with 106 additions and 5 deletions

View file

@ -16,10 +16,14 @@ LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.authors="andras@schmelczer.dev"
COPY --from=builder /build/local-client-cli/dist/cli.js /app/cli.js
COPY --from=builder /build/local-client-cli/dist/healthcheck.js /app/healthcheck.js
HEALTHCHECK --interval=10s --timeout=5s --start-period=10s --retries=1 \
CMD node /app/healthcheck.js /tmp/vaultlink-health.json
WORKDIR /vault
VOLUME ["/vault"]
ENTRYPOINT ["node", "/app/cli.js"]
ENTRYPOINT ["node", "/app/cli.js", "--health", "/tmp/vaultlink-health.json"]
CMD ["--help"]

View file

@ -12,6 +12,7 @@ export interface CliArgs {
ignorePatterns?: string[];
webSocketRetryIntervalMs?: number;
logLevel: LogLevel;
health?: string;
}
export function parseArgs(argv: string[]): CliArgs {
@ -51,6 +52,10 @@ export function parseArgs(argv: string[]): CliArgs {
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
"INFO"
)
.option(
"--health <path>",
"[OPTIONAL] Path to health status file for Docker healthcheck"
)
.addHelpText(
"after",
`
@ -78,6 +83,7 @@ Examples:
| number
| undefined;
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
const health = opts.health as string | undefined;
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
if (localPath === undefined) {
@ -117,6 +123,7 @@ Examples:
maxFileSizeMB: maxFileSizeMb,
ignorePatterns: ignorePattern,
webSocketRetryIntervalMs: websocketRetryIntervalMs,
logLevel
logLevel,
health
};
}

View file

@ -1,5 +1,7 @@
import * as path from "path";
import * as fs from "fs/promises";
import * as fsSync from "fs";
import type { NetworkConnectionStatus } from "sync-client";
import {
SyncClient,
DEFAULT_SETTINGS,
@ -13,6 +15,19 @@ import { FileWatcher } from "./file-watcher";
import { formatLogLine, colorize, styleText } from "./logger-formatter";
import packageJson from "../package.json";
function writeHealthStatus(
filePath: string,
connectionStatus: NetworkConnectionStatus
): void {
try {
fsSync.writeFileSync(filePath, JSON.stringify(connectionStatus));
} catch (error) {
console.error(
`Failed to write health status to ${filePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
const LOG_LEVEL_ORDER = {
[LogLevel.DEBUG]: 0,
[LogLevel.INFO]: 1,
@ -78,6 +93,7 @@ async function main(): Promise<void> {
syncConcurrency:
args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency,
maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB,
diffCacheSizeMB: DEFAULT_SETTINGS.diffCacheSizeMB,
ignorePatterns,
webSocketRetryIntervalMs:
args.webSocketRetryIntervalMs ??
@ -119,6 +135,15 @@ async function main(): Promise<void> {
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
});
if (args.health !== undefined) {
const healthFile = args.health;
setInterval(() => {
void client.checkConnection().then((status) => {
writeHealthStatus(healthFile, status);
});
}, 30 * 1000); // every 30 seconds
}
// Add colored log formatter with level filtering
client.logger.addOnMessageListener((logLine) => {
// Only show messages at or above the configured log level
@ -132,7 +157,10 @@ async function main(): Promise<void> {
const fileWatcher = new FileWatcher(absolutePath, client);
client.addWebSocketStatusChangeListener(() => {
client.logger.info("WebSocket status changed");
const isConnected = client.isWebSocketConnected;
client.logger.info(
`WebSocket status changed: ${isConnected ? "connected" : "disconnected"}`
);
});
client.addRemainingSyncOperationsListener((remaining) => {

View file

@ -0,0 +1,59 @@
#!/usr/bin/env node
/**
* Healthcheck script for Docker container
* Checks if the sync client is connected to the server
*/
import * as fs from "fs";
import type { NetworkConnectionStatus } from "sync-client";
function isHealthStatus(value: unknown): value is NetworkConnectionStatus {
if (typeof value !== "object" || value === null) {
return false;
}
return true;
}
function main(): void {
if (process.argv.length < 3) {
console.error("Usage: healthcheck <path-to-health-file>");
process.exit(1);
}
const [, , healthFile] = process.argv;
try {
// Check if health file exists
if (!fs.existsSync(healthFile)) {
console.error(`Health file does not exist: ${healthFile}`);
process.exit(1);
}
// Read and parse health status
const content = fs.readFileSync(healthFile, "utf-8");
const parsed: unknown = JSON.parse(content);
// Validate the parsed object using type guard
if (!isHealthStatus(parsed)) {
throw new Error("Invalid health status format");
}
const status = parsed;
if (!status.isSuccessful || !status.isWebSocketConnected) {
console.error("Not connected to server: " + status.serverMessage);
process.exit(1);
}
console.log("Healthy: Connected to server");
process.exit(0);
} catch (error) {
console.error(
`Health check failed: ${error instanceof Error ? error.message : String(error)}`
);
process.exit(1);
}
}
main();

View file

@ -2,7 +2,10 @@ const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src/cli.ts",
entry: {
cli: "./src/cli.ts",
healthcheck: "./src/healthcheck.ts"
},
target: "node",
mode: "production",
optimization: {
@ -21,7 +24,7 @@ module.exports = {
},
output: {
globalObject: "this",
filename: "cli.js",
filename: "[name].js",
path: path.resolve(__dirname, "dist")
},
plugins: [