diff --git a/changelogs/2.5.2.md b/changelogs/2.5.2.md new file mode 100644 index 0000000..1048632 --- /dev/null +++ b/changelogs/2.5.2.md @@ -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. diff --git a/command/logs.ts b/command/logs.ts index 2ab2f74..f575cb9 100644 --- a/command/logs.ts +++ b/command/logs.ts @@ -86,7 +86,7 @@ export async function runLogs( write(service ?? event.pod ?? project, event.message, true); continue; } - write(event.targetName || service || project, event.message, false); + write(event.pod || event.targetName || service || project, event.message, false); } } diff --git a/command/main.ts b/command/main.ts index 1abbc86..908f2ba 100644 --- a/command/main.ts +++ b/command/main.ts @@ -37,7 +37,7 @@ function cloneCommand(command: T): T { export const main = defineCommand({ meta: { name: "kuber", - version: "2.5.1", + version: "2.5.2-rc.1", description: "Docker Compose -> K8s translation layer", }, args: { diff --git a/package.json b/package.json index 8cd4e10..9288da2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dmgnr/kuber", - "version": "2.5.1", + "version": "2.5.2-rc.1", "description": "Docker Compose to Kubernetes translation layer", "bin": { "kuber": "dist/index.js" diff --git a/server/kubernetes-logs.ts b/server/kubernetes-logs.ts index d399208..8b4493d 100644 --- a/server/kubernetes-logs.ts +++ b/server/kubernetes-logs.ts @@ -40,10 +40,19 @@ function logError(error: unknown): KubernetesLogError { error && typeof error === "object" && "code" in error ? error.code : 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( error instanceof Error ? error.message : "Kubernetes log request failed", - code === undefined && - (status === undefined || status === 408 || status === 429 || status >= 500), + typeof code === "string" + ? transientNetworkCodes.has(code) + : status === undefined && /socket hang up|socket closed/i.test(message) + ? true + : status === undefined || status === 408 || status === 429 || status >= 500, ); } diff --git a/server/log-service.ts b/server/log-service.ts index 8ee1b32..0935202 100644 --- a/server/log-service.ts +++ b/server/log-service.ts @@ -438,7 +438,7 @@ export class LogService { const discoveryInterval = positiveInteger(input.discoveryIntervalMs, 2_000); const heartbeatInterval = positiveInteger( input.heartbeatIntervalMs, - 15_000, + 5_000, ); const retryInterval = positiveInteger(input.retryIntervalMs, 1_000); const queue = new BoundedAsyncQueue(capacity); @@ -489,7 +489,11 @@ export class LogService { signal, (event) => queue.push(event), ); - if (!signal.aborted) completed.add(key); + if (!signal.aborted) + throw new KubernetesLogError( + "Kubernetes log stream ended unexpectedly", + true, + ); } catch (error) { if (!isAbort(signal)) { await pushError(error, source); diff --git a/tests/command/logs.test.ts b/tests/command/logs.test.ts index bc6f290..7f28610 100644 --- a/tests/command/logs.test.ts +++ b/tests/command/logs.test.ts @@ -58,7 +58,7 @@ describe("logs API command runner", () => { }, ]); expect(output).toEqual([ - ["web", "ready", false], + ["web-1", "ready", false], ["web api", "disconnected", true], ]); }); diff --git a/tests/server/log-service.test.ts b/tests/server/log-service.test.ts index f1907e0..ba77c19 100644 --- a/tests/server/log-service.test.ts +++ b/tests/server/log-service.test.ts @@ -650,7 +650,7 @@ describe("LogService", () => { expect(error).toMatchObject({ pod: "web-abc", container: "web", - retryable: false, + retryable: true, }); expect(error?.message).toMatch( /socket.*connection.*closed|socket hang up|fetch failed/i,