Files
kuber/command/logs.ts
T
2026-09-03 11:28:30 +07:00

112 lines
2.7 KiB
TypeScript

import { defineCommand } from "citty";
import {
apiStreamNdjson,
type ApiRequestInit,
type ApiRequestOptions,
} from "../lib/api";
import { ctx } from "../lib/context";
import { createLogger } from "../lib/logger";
export type LogApiEvent =
| {
type: "log";
targetName: string;
pod: string;
container: string;
message: string;
}
| { type: "heartbeat"; timestamp: string }
| {
type: "error";
message: string;
pod?: string;
container?: string;
retryable: boolean;
};
export type LogsApiStream = <T>(
path: string,
init?: ApiRequestInit,
options?: ApiRequestOptions,
) => AsyncIterable<T>;
export type LogEventWriter = (
prefix: string,
message: string,
error: boolean,
) => void;
function defaultWriter(): LogEventWriter {
const loggers = new Map<string, ReturnType<typeof createLogger>>();
return (prefix, message, error) => {
let logger = loggers.get(prefix);
if (!logger) {
logger = createLogger(prefix);
loggers.set(prefix, logger);
}
if (error) logger.warn`${message}`;
else logger`${message}`;
};
}
export async function runLogs(
project: string,
service: string | undefined,
follow: boolean,
signal?: AbortSignal,
stream: LogsApiStream = apiStreamNdjson,
write: LogEventWriter = defaultWriter(),
): Promise<void> {
const query = new URLSearchParams();
if (service) query.set("service", service);
if (follow) query.set("follow", "true");
const suffix = query.size ? `?${query}` : "";
for await (const event of stream<LogApiEvent>(
`/workspaces/${encodeURIComponent(project)}/logs${suffix}`,
{ signal },
{ timeoutMs: follow ? 0 : undefined },
)) {
if (event.type === "heartbeat") continue;
if (event.type === "error") {
write(service ?? event.pod ?? project, event.message, true);
continue;
}
write(event.targetName || service || project, event.message, false);
}
}
export const logs = defineCommand({
meta: {
name: "logs",
description: "Show deployment logs",
},
args: {
follow: {
type: "boolean",
alias: "f",
description: "Follow log output",
},
},
async run({ args }) {
const service = args._[0];
const controller = new AbortController();
const abort = () => controller.abort();
process.on("SIGINT", abort);
process.on("SIGTERM", abort);
try {
await runLogs(
ctx().project,
service,
Boolean(args.follow),
controller.signal,
);
} catch (error) {
if (!controller.signal.aborted) throw error;
} finally {
process.off("SIGINT", abort);
process.off("SIGTERM", abort);
}
},
});