|
1 |
| -import { AxiosInstance } from "axios"; |
| 1 | +import { AxiosInstance, InternalAxiosRequestConfig, isAxiosError } from "axios"; |
2 | 2 | import { spawn } from "child_process";
|
3 | 3 | import { Api } from "coder/site/src/api/api";
|
4 | 4 | import {
|
5 | 5 | ProvisionerJobLog,
|
6 | 6 | Workspace,
|
7 | 7 | } from "coder/site/src/api/typesGenerated";
|
8 |
| -import { FetchLikeInit } from "eventsource"; |
9 | 8 | import fs from "fs/promises";
|
10 | 9 | import { ProxyAgent } from "proxy-agent";
|
11 | 10 | import * as vscode from "vscode";
|
@@ -83,6 +82,9 @@ export function makeCoderSdk(
|
83 | 82 | restClient.setSessionToken(token);
|
84 | 83 | }
|
85 | 84 |
|
| 85 | + // Logging interceptor |
| 86 | + addLoggingInterceptors(restClient.getAxiosInstance(), storage.output); |
| 87 | + |
86 | 88 | restClient.getAxiosInstance().interceptors.request.use(async (config) => {
|
87 | 89 | // Add headers from the header command.
|
88 | 90 | Object.entries(await storage.getHeaders(baseUrl)).forEach(
|
@@ -113,57 +115,75 @@ export function makeCoderSdk(
|
113 | 115 | return restClient;
|
114 | 116 | }
|
115 | 117 |
|
116 |
| -/** |
117 |
| - * Creates a fetch adapter using an Axios instance that returns streaming responses. |
118 |
| - * This can be used with APIs that accept fetch-like interfaces. |
119 |
| - */ |
120 |
| -export function createStreamingFetchAdapter(axiosInstance: AxiosInstance) { |
121 |
| - return async (url: string | URL, init?: FetchLikeInit) => { |
122 |
| - const urlStr = url.toString(); |
123 |
| - |
124 |
| - const response = await axiosInstance.request({ |
125 |
| - url: urlStr, |
126 |
| - signal: init?.signal, |
127 |
| - headers: init?.headers as Record<string, string>, |
128 |
| - responseType: "stream", |
129 |
| - validateStatus: () => true, // Don't throw on any status code |
130 |
| - }); |
131 |
| - const stream = new ReadableStream({ |
132 |
| - start(controller) { |
133 |
| - response.data.on("data", (chunk: Buffer) => { |
134 |
| - controller.enqueue(chunk); |
135 |
| - }); |
| 118 | +interface RequestConfigWithMetadata extends InternalAxiosRequestConfig { |
| 119 | + metadata?: { |
| 120 | + requestId: string; |
| 121 | + startedAt: number; |
| 122 | + }; |
| 123 | +} |
136 | 124 |
|
137 |
| - response.data.on("end", () => { |
138 |
| - controller.close(); |
139 |
| - }); |
| 125 | +function addLoggingInterceptors( |
| 126 | + client: AxiosInstance, |
| 127 | + logger: vscode.LogOutputChannel, |
| 128 | +) { |
| 129 | + client.interceptors.request.use( |
| 130 | + (config) => { |
| 131 | + const requestId = crypto.randomUUID(); |
| 132 | + (config as RequestConfigWithMetadata).metadata = { |
| 133 | + requestId, |
| 134 | + startedAt: Date.now(), |
| 135 | + }; |
140 | 136 |
|
141 |
| - response.data.on("error", (err: Error) => { |
142 |
| - controller.error(err); |
143 |
| - }); |
144 |
| - }, |
| 137 | + logger.trace( |
| 138 | + `Request ${requestId}: ${config.method?.toUpperCase()} ${config.url}`, |
| 139 | + config.data ?? "", |
| 140 | + ); |
145 | 141 |
|
146 |
| - cancel() { |
147 |
| - response.data.destroy(); |
148 |
| - return Promise.resolve(); |
149 |
| - }, |
150 |
| - }); |
| 142 | + return config; |
| 143 | + }, |
| 144 | + (error: unknown) => { |
| 145 | + let message: string = "Request error"; |
| 146 | + if (isAxiosError(error)) { |
| 147 | + const meta = (error.config as RequestConfigWithMetadata)?.metadata; |
| 148 | + const requestId = meta?.requestId ?? "n/a"; |
| 149 | + message = `Request ${requestId} error`; |
| 150 | + } |
| 151 | + logger.warn(message, error); |
151 | 152 |
|
152 |
| - return { |
153 |
| - body: { |
154 |
| - getReader: () => stream.getReader(), |
155 |
| - }, |
156 |
| - url: urlStr, |
157 |
| - status: response.status, |
158 |
| - redirected: response.request.res.responseUrl !== urlStr, |
159 |
| - headers: { |
160 |
| - get: (name: string) => { |
161 |
| - const value = response.headers[name.toLowerCase()]; |
162 |
| - return value === undefined ? null : String(value); |
163 |
| - }, |
164 |
| - }, |
165 |
| - }; |
166 |
| - }; |
| 153 | + return Promise.reject(error); |
| 154 | + }, |
| 155 | + ); |
| 156 | + |
| 157 | + client.interceptors.response.use( |
| 158 | + (response) => { |
| 159 | + const { requestId, startedAt } = |
| 160 | + (response.config as RequestConfigWithMetadata).metadata ?? {}; |
| 161 | + const ms = startedAt ? Date.now() - startedAt : undefined; |
| 162 | + |
| 163 | + logger.trace( |
| 164 | + `Response ${requestId ?? "n/a"}: ${response.status}${ |
| 165 | + ms !== undefined ? ` in ${ms}ms` : "" |
| 166 | + }`, |
| 167 | + // { responseBody: response.data }, // TODO too noisy |
| 168 | + ); |
| 169 | + return response; |
| 170 | + }, |
| 171 | + (error: unknown) => { |
| 172 | + let message = "Response error"; |
| 173 | + if (isAxiosError(error)) { |
| 174 | + const { metadata } = (error.config as RequestConfigWithMetadata) ?? {}; |
| 175 | + const requestId = metadata?.requestId ?? "n/a"; |
| 176 | + const startedAt = metadata?.startedAt; |
| 177 | + const ms = startedAt ? Date.now() - startedAt : undefined; |
| 178 | + |
| 179 | + const status = error.response?.status ?? "unknown"; |
| 180 | + message = `Response ${requestId}: ${status}${ms !== undefined ? ` in ${ms}ms` : ""}`; |
| 181 | + } |
| 182 | + logger.warn(message, error); |
| 183 | + |
| 184 | + return Promise.reject(error); |
| 185 | + }, |
| 186 | + ); |
167 | 187 | }
|
168 | 188 |
|
169 | 189 | /**
|
@@ -269,6 +289,7 @@ export async function waitForBuild(
|
269 | 289 | const agent = await createHttpAgent();
|
270 | 290 | await new Promise<void>((resolve, reject) => {
|
271 | 291 | try {
|
| 292 | + // TODO move to `ws-helper` |
272 | 293 | const baseUrl = new URL(baseUrlRaw);
|
273 | 294 | const proto = baseUrl.protocol === "https:" ? "wss:" : "ws:";
|
274 | 295 | const socketUrlRaw = `${proto}//${baseUrl.host}${path}`;
|
|
0 commit comments