fix: improve egress and job diagnostics (#969)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-26 12:51:58 +08:00
committed by GitHub
parent d1c189e498
commit d3d542fbd3
10 changed files with 375 additions and 41 deletions
@@ -1,3 +1,5 @@
// SPEC: PJ2026-01060509 出站诊断 draft-2026-06-26-p8-egress-job-friction.
// Provider-side egress TCP proxy with lifecycle logs for tunnel diagnosis.
import { createServer, type Server, type Socket } from "node:net";
import type {
CoreEgressTcpCloseMessage,
@@ -33,6 +35,8 @@ interface Tunnel {
closed: boolean;
createdAt: number;
lastActivityAt: number;
targetHost: string;
targetPort: number;
openTimer: ReturnType<typeof setTimeout> | null;
idleTimer: ReturnType<typeof setTimeout> | null;
}
@@ -116,6 +120,18 @@ function proxyUrlFor(host: string, port: number): string {
return `http://${host}:${port}`;
}
function egressFailureKind(error: string | undefined): string {
if (error === undefined || error.length === 0) return "normal-close";
const normalized = error.toLowerCase();
if (normalized.includes("open timeout")) return "open-timeout";
if (normalized.includes("idle timeout")) return "idle-timeout";
if (normalized.includes("pending buffer")) return "pending-buffer-exceeded";
if (normalized.includes("not connected")) return "core-channel-not-connected";
if (normalized.includes("timeout")) return "timeout";
if (normalized.includes("closed")) return "closed";
return "remote-or-socket-error";
}
const tunnelOpenTimeoutMs = 15_000;
const tunnelIdleTimeoutMs = 600_000;
const maxPendingBytes = 4 * 1024 * 1024;
@@ -163,14 +179,19 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
tunnel.pendingBytes = 0;
if (!tunnel.client.destroyed) tunnel.client.destroy();
if (notifyCore) send({ type: "egress_tcp_close", providerId: options.providerId, connectionId: tunnel.id, at: nowIso() });
if (error !== undefined) {
options.logger("warn", "egress_proxy_tunnel_closed", {
connectionId: tunnel.id,
opened: tunnel.opened,
ageMs: Date.now() - tunnel.createdAt,
error,
});
}
const event = {
providerId: options.providerId,
connectionId: tunnel.id,
method: tunnel.method,
targetHost: tunnel.targetHost,
targetPort: tunnel.targetPort,
opened: tunnel.opened,
ageMs: Date.now() - tunnel.createdAt,
pendingBytes: tunnel.pendingBytes,
failureKind: egressFailureKind(error),
...(error === undefined ? {} : { error }),
};
options.logger(error === undefined ? "info" : "warn", "egress_proxy_tunnel_closed", event);
};
const closeTunnel = (id: string, error?: string): void => {
@@ -206,6 +227,15 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
tunnel.openTimer = null;
}
refreshTunnelIdle(tunnel);
options.logger("info", "egress_proxy_tunnel_opened", {
providerId: options.providerId,
connectionId: tunnel.id,
method: tunnel.method,
targetHost: tunnel.targetHost,
targetPort: tunnel.targetPort,
ageMs: Date.now() - tunnel.createdAt,
pendingBytes: tunnel.pendingBytes,
});
if (tunnel.method === "CONNECT") {
tunnel.client.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: UniDesk-ProviderGateway\r\n\r\n");
}
@@ -226,7 +256,14 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
destroyTunnel(tunnel, false, message.error);
}
if (message.error !== undefined && message.error.length > 0) {
options.logger("warn", "egress_proxy_remote_close", { connectionId: message.connectionId, error: message.error });
options.logger("warn", "egress_proxy_remote_close", {
providerId: options.providerId,
connectionId: message.connectionId,
targetHost: tunnel?.targetHost ?? null,
targetPort: tunnel?.targetPort ?? null,
failureKind: egressFailureKind(message.error),
error: message.error,
});
}
return true;
}
@@ -295,10 +332,20 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
closed: false,
createdAt,
lastActivityAt: createdAt,
targetHost: target.host,
targetPort: target.port,
openTimer: null,
idleTimer: null,
};
tunnels.set(id, tunnel);
options.logger("info", "egress_proxy_tunnel_open_request", {
providerId: options.providerId,
connectionId: id,
method: parsed.method,
targetHost: target.host,
targetPort: target.port,
firstPayloadBytes: firstPayload === null ? 0 : firstPayload.byteLength,
});
client.on("data", (nextChunk) => {
const nextBuffer = Buffer.isBuffer(nextChunk) ? nextChunk : Buffer.from(nextChunk);
if (!tunnel.opened) {
@@ -315,6 +362,14 @@ export function startProviderEgressProxy(options: ProviderEgressProxyOptions): P
tunnels.delete(id);
tunnel.closed = true;
clearTunnelTimers(tunnel);
options.logger("warn", "egress_proxy_tunnel_open_not_sent", {
providerId: options.providerId,
connectionId: id,
method: parsed.method,
targetHost: target.host,
targetPort: target.port,
failureKind: "core-channel-not-connected",
});
fail("503 Service Unavailable", "provider-gateway core channel is not connected\n");
return;
}