fix: keep pod log streams alive

This commit is contained in:
2026-09-27 11:37:02 +00:00 Unverified
parent 5f7509a9c5
commit d3e9753c4b
8 changed files with 32 additions and 9 deletions
+10
View File
@@ -0,0 +1,10 @@
# 2.5.2-rc.1
## Fixed
- Keep server-side Kubernetes log streams active with a 5-second idle heartbeat, below common intermediary idle timeouts.
- Retry transient Kubernetes log socket and stream closures; permanent TLS certificate errors remain terminal.
## Changed
- Prefix CLI log lines with the originating pod name instead of the target service name.
+1 -1
View File
@@ -86,7 +86,7 @@ export async function runLogs(
write(service ?? event.pod ?? project, event.message, true); write(service ?? event.pod ?? project, event.message, true);
continue; continue;
} }
write(event.targetName || service || project, event.message, false); write(event.pod || event.targetName || service || project, event.message, false);
} }
} }
+1 -1
View File
@@ -37,7 +37,7 @@ function cloneCommand<T extends object>(command: T): T {
export const main = defineCommand({ export const main = defineCommand({
meta: { meta: {
name: "kuber", name: "kuber",
version: "2.5.1", version: "2.5.2-rc.1",
description: "Docker Compose -> K8s translation layer", description: "Docker Compose -> K8s translation layer",
}, },
args: { args: {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@dmgnr/kuber", "name": "@dmgnr/kuber",
"version": "2.5.1", "version": "2.5.2-rc.1",
"description": "Docker Compose to Kubernetes translation layer", "description": "Docker Compose to Kubernetes translation layer",
"bin": { "bin": {
"kuber": "dist/index.js" "kuber": "dist/index.js"
+11 -2
View File
@@ -40,10 +40,19 @@ function logError(error: unknown): KubernetesLogError {
error && typeof error === "object" && "code" in error error && typeof error === "object" && "code" in error
? error.code ? error.code
: undefined; : undefined;
const transientNetworkCodes = new Set([
"ECONNRESET", "ETIMEDOUT", "EPIPE", "ECONNREFUSED", "EHOSTUNREACH",
"ENETUNREACH", "ECONNABORTED", "EAI_AGAIN", "UND_ERR_SOCKET",
"UND_ERR_CONNECT_TIMEOUT",
]);
const message = error instanceof Error ? error.message : "";
return new KubernetesLogError( return new KubernetesLogError(
error instanceof Error ? error.message : "Kubernetes log request failed", error instanceof Error ? error.message : "Kubernetes log request failed",
code === undefined && typeof code === "string"
(status === undefined || status === 408 || status === 429 || status >= 500), ? transientNetworkCodes.has(code)
: status === undefined && /socket hang up|socket closed/i.test(message)
? true
: status === undefined || status === 408 || status === 429 || status >= 500,
); );
} }
+6 -2
View File
@@ -438,7 +438,7 @@ export class LogService {
const discoveryInterval = positiveInteger(input.discoveryIntervalMs, 2_000); const discoveryInterval = positiveInteger(input.discoveryIntervalMs, 2_000);
const heartbeatInterval = positiveInteger( const heartbeatInterval = positiveInteger(
input.heartbeatIntervalMs, input.heartbeatIntervalMs,
15_000, 5_000,
); );
const retryInterval = positiveInteger(input.retryIntervalMs, 1_000); const retryInterval = positiveInteger(input.retryIntervalMs, 1_000);
const queue = new BoundedAsyncQueue<LogEvent>(capacity); const queue = new BoundedAsyncQueue<LogEvent>(capacity);
@@ -489,7 +489,11 @@ export class LogService {
signal, signal,
(event) => queue.push(event), (event) => queue.push(event),
); );
if (!signal.aborted) completed.add(key); if (!signal.aborted)
throw new KubernetesLogError(
"Kubernetes log stream ended unexpectedly",
true,
);
} catch (error) { } catch (error) {
if (!isAbort(signal)) { if (!isAbort(signal)) {
await pushError(error, source); await pushError(error, source);
+1 -1
View File
@@ -58,7 +58,7 @@ describe("logs API command runner", () => {
}, },
]); ]);
expect(output).toEqual([ expect(output).toEqual([
["web", "ready", false], ["web-1", "ready", false],
["web api", "disconnected", true], ["web api", "disconnected", true],
]); ]);
}); });
+1 -1
View File
@@ -650,7 +650,7 @@ describe("LogService", () => {
expect(error).toMatchObject({ expect(error).toMatchObject({
pod: "web-abc", pod: "web-abc",
container: "web", container: "web",
retryable: false, retryable: true,
}); });
expect(error?.message).toMatch( expect(error?.message).toMatch(
/socket.*connection.*closed|socket hang up|fetch failed/i, /socket.*connection.*closed|socket hang up|fetch failed/i,