Add healthcheck for client
This commit is contained in:
parent
418e09f08a
commit
c92aa63d71
5 changed files with 106 additions and 5 deletions
|
|
@ -16,10 +16,14 @@ LABEL org.opencontainers.image.licenses="MIT"
|
||||||
LABEL org.opencontainers.image.authors="andras@schmelczer.dev"
|
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/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
|
WORKDIR /vault
|
||||||
|
|
||||||
VOLUME ["/vault"]
|
VOLUME ["/vault"]
|
||||||
|
|
||||||
ENTRYPOINT ["node", "/app/cli.js"]
|
ENTRYPOINT ["node", "/app/cli.js", "--health", "/tmp/vaultlink-health.json"]
|
||||||
CMD ["--help"]
|
CMD ["--help"]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export interface CliArgs {
|
||||||
ignorePatterns?: string[];
|
ignorePatterns?: string[];
|
||||||
webSocketRetryIntervalMs?: number;
|
webSocketRetryIntervalMs?: number;
|
||||||
logLevel: LogLevel;
|
logLevel: LogLevel;
|
||||||
|
health?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseArgs(argv: string[]): CliArgs {
|
export function parseArgs(argv: string[]): CliArgs {
|
||||||
|
|
@ -51,6 +52,10 @@ export function parseArgs(argv: string[]): CliArgs {
|
||||||
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
|
"[OPTIONAL] Log level (DEBUG, INFO, WARNING, ERROR)",
|
||||||
"INFO"
|
"INFO"
|
||||||
)
|
)
|
||||||
|
.option(
|
||||||
|
"--health <path>",
|
||||||
|
"[OPTIONAL] Path to health status file for Docker healthcheck"
|
||||||
|
)
|
||||||
.addHelpText(
|
.addHelpText(
|
||||||
"after",
|
"after",
|
||||||
`
|
`
|
||||||
|
|
@ -78,6 +83,7 @@ Examples:
|
||||||
| number
|
| number
|
||||||
| undefined;
|
| undefined;
|
||||||
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
|
const logLevelStr = (opts.logLevel as string | undefined) ?? "INFO";
|
||||||
|
const health = opts.health as string | undefined;
|
||||||
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
|
/* eslint-enable @typescript-eslint/no-unsafe-type-assertion */
|
||||||
|
|
||||||
if (localPath === undefined) {
|
if (localPath === undefined) {
|
||||||
|
|
@ -117,6 +123,7 @@ Examples:
|
||||||
maxFileSizeMB: maxFileSizeMb,
|
maxFileSizeMB: maxFileSizeMb,
|
||||||
ignorePatterns: ignorePattern,
|
ignorePatterns: ignorePattern,
|
||||||
webSocketRetryIntervalMs: websocketRetryIntervalMs,
|
webSocketRetryIntervalMs: websocketRetryIntervalMs,
|
||||||
logLevel
|
logLevel,
|
||||||
|
health
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
|
import * as fsSync from "fs";
|
||||||
|
import type { NetworkConnectionStatus } from "sync-client";
|
||||||
import {
|
import {
|
||||||
SyncClient,
|
SyncClient,
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
|
|
@ -13,6 +15,19 @@ import { FileWatcher } from "./file-watcher";
|
||||||
import { formatLogLine, colorize, styleText } from "./logger-formatter";
|
import { formatLogLine, colorize, styleText } from "./logger-formatter";
|
||||||
import packageJson from "../package.json";
|
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 = {
|
const LOG_LEVEL_ORDER = {
|
||||||
[LogLevel.DEBUG]: 0,
|
[LogLevel.DEBUG]: 0,
|
||||||
[LogLevel.INFO]: 1,
|
[LogLevel.INFO]: 1,
|
||||||
|
|
@ -78,6 +93,7 @@ async function main(): Promise<void> {
|
||||||
syncConcurrency:
|
syncConcurrency:
|
||||||
args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency,
|
args.syncConcurrency ?? DEFAULT_SETTINGS.syncConcurrency,
|
||||||
maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB,
|
maxFileSizeMB: args.maxFileSizeMB ?? DEFAULT_SETTINGS.maxFileSizeMB,
|
||||||
|
diffCacheSizeMB: DEFAULT_SETTINGS.diffCacheSizeMB,
|
||||||
ignorePatterns,
|
ignorePatterns,
|
||||||
webSocketRetryIntervalMs:
|
webSocketRetryIntervalMs:
|
||||||
args.webSocketRetryIntervalMs ??
|
args.webSocketRetryIntervalMs ??
|
||||||
|
|
@ -119,6 +135,15 @@ async function main(): Promise<void> {
|
||||||
nativeLineEndings: process.platform === "win32" ? "\r\n" : "\n"
|
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
|
// Add colored log formatter with level filtering
|
||||||
client.logger.addOnMessageListener((logLine) => {
|
client.logger.addOnMessageListener((logLine) => {
|
||||||
// Only show messages at or above the configured log level
|
// 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);
|
const fileWatcher = new FileWatcher(absolutePath, client);
|
||||||
|
|
||||||
client.addWebSocketStatusChangeListener(() => {
|
client.addWebSocketStatusChangeListener(() => {
|
||||||
client.logger.info("WebSocket status changed");
|
const isConnected = client.isWebSocketConnected;
|
||||||
|
client.logger.info(
|
||||||
|
`WebSocket status changed: ${isConnected ? "connected" : "disconnected"}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.addRemainingSyncOperationsListener((remaining) => {
|
client.addRemainingSyncOperationsListener((remaining) => {
|
||||||
|
|
|
||||||
59
frontend/local-client-cli/src/healthcheck.ts
Normal file
59
frontend/local-client-cli/src/healthcheck.ts
Normal 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();
|
||||||
|
|
@ -2,7 +2,10 @@ const path = require("path");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: "./src/cli.ts",
|
entry: {
|
||||||
|
cli: "./src/cli.ts",
|
||||||
|
healthcheck: "./src/healthcheck.ts"
|
||||||
|
},
|
||||||
target: "node",
|
target: "node",
|
||||||
mode: "production",
|
mode: "production",
|
||||||
optimization: {
|
optimization: {
|
||||||
|
|
@ -21,7 +24,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
globalObject: "this",
|
globalObject: "this",
|
||||||
filename: "cli.js",
|
filename: "[name].js",
|
||||||
path: path.resolve(__dirname, "dist")
|
path: path.resolve(__dirname, "dist")
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue