Skip to content

Commit d33d380

Browse files
committed
Refactor the logging and add coder.httpClientLogLevel setting
1 parent e97e687 commit d33d380

16 files changed

+385
-232
lines changed

package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,23 @@
126126
"items": {
127127
"type": "string"
128128
}
129+
},
130+
"coder.httpClientLogLevel": {
131+
"markdownDescription": "Controls the verbosity of HTTP client logging. This affects what details are logged for each HTTP request and response.",
132+
"type": "string",
133+
"enum": [
134+
"none",
135+
"basic",
136+
"headers",
137+
"body"
138+
],
139+
"markdownEnumDescriptions": [
140+
"Disables all HTTP client logging",
141+
"Logs the request method, URL, length, and the response status code",
142+
"Logs everything from *basic* plus sanitized request and response headers",
143+
"Logs everything from *headers* plus request and response bodies (may include sensitive data)"
144+
],
145+
"default": "basic"
129146
}
130147
}
131148
},

src/agentMetadataHelper.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ export function createAgentMetadataWatcher(
6464

6565
socket.addEventListener("error", handleError);
6666

67+
socket.addEventListener("close", (event) => {
68+
if (event.code !== 1000) {
69+
handleError(
70+
new Error(
71+
`WebSocket closed unexpectedly: ${event.code} ${event.reason}`,
72+
),
73+
);
74+
}
75+
});
76+
6777
return watcher;
6878
}
6979

src/api/codeApi.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,30 @@ import { type WorkspaceConfiguration } from "vscode";
1111
import { ClientOptions } from "ws";
1212
import { CertificateError } from "../error";
1313
import { getHeaderCommand, getHeaders } from "../headers";
14-
import { Logger } from "../logging/logger";
1514
import {
1615
createRequestMeta,
16+
logRequest,
1717
logRequestError,
18-
logRequestStart,
19-
logRequestSuccess,
20-
RequestConfigWithMeta,
21-
WsLogger,
22-
} from "../logging/netLog";
23-
import { OneWayCodeWebSocket } from "../websocket/oneWayCodeWebSocket";
18+
logResponse,
19+
} from "../logging/httpLogger";
20+
import { Logger } from "../logging/logger";
21+
import { RequestConfigWithMeta, HttpLogLevel } from "../logging/types";
22+
import { WsLogger } from "../logging/wsLogger";
23+
import { OneWayWebSocket } from "../websocket/oneWayWebSocket";
2424
import { createHttpAgent } from "./auth";
2525

2626
const coderSessionTokenHeader = "Coder-Session-Token";
2727

28+
type WorkspaceConfigurationProvider = () => WorkspaceConfiguration;
29+
2830
/**
2931
* Unified API class that includes both REST API methods from the base Api class
3032
* and WebSocket methods for real-time functionality.
3133
*/
3234
export class CodeApi extends Api {
3335
private constructor(
3436
private readonly output: Logger,
35-
private readonly cfg: WorkspaceConfiguration,
37+
private readonly configProvider: WorkspaceConfigurationProvider,
3638
) {
3739
super();
3840
}
@@ -45,15 +47,15 @@ export class CodeApi extends Api {
4547
baseUrl: string,
4648
token: string | undefined,
4749
output: Logger,
48-
cfg: WorkspaceConfiguration,
50+
configProvider: WorkspaceConfigurationProvider,
4951
): CodeApi {
50-
const client = new CodeApi(output, cfg);
52+
const client = new CodeApi(output, configProvider);
5153
client.setHost(baseUrl);
5254
if (token) {
5355
client.setSessionToken(token);
5456
}
5557

56-
setupInterceptors(client, baseUrl, output, cfg);
58+
setupInterceptors(client, baseUrl, output, configProvider);
5759
return client;
5860
}
5961

@@ -120,8 +122,8 @@ export class CodeApi extends Api {
120122
coderSessionTokenHeader
121123
] as string | undefined;
122124

123-
const httpAgent = createHttpAgent(this.cfg);
124-
const webSocket = new OneWayCodeWebSocket<TData>({
125+
const httpAgent = createHttpAgent(this.configProvider());
126+
const webSocket = new OneWayWebSocket<TData>({
125127
location: baseUrl,
126128
...configs,
127129
options: {
@@ -167,12 +169,16 @@ function setupInterceptors(
167169
client: CodeApi,
168170
baseUrl: string,
169171
output: Logger,
170-
cfg: WorkspaceConfiguration,
172+
configProvider: WorkspaceConfigurationProvider,
171173
): void {
172-
addLoggingInterceptors(client.getAxiosInstance(), output);
174+
addLoggingInterceptors(client.getAxiosInstance(), output, configProvider);
173175

174176
client.getAxiosInstance().interceptors.request.use(async (config) => {
175-
const headers = await getHeaders(baseUrl, getHeaderCommand(cfg), output);
177+
const headers = await getHeaders(
178+
baseUrl,
179+
getHeaderCommand(configProvider()),
180+
output,
181+
);
176182
// Add headers from the header command.
177183
Object.entries(headers).forEach(([key, value]) => {
178184
config.headers[key] = value;
@@ -181,7 +187,7 @@ function setupInterceptors(
181187
// Configure proxy and TLS.
182188
// Note that by default VS Code overrides the agent. To prevent this, set
183189
// `http.proxySupport` to `on` or `off`.
184-
const agent = createHttpAgent(cfg);
190+
const agent = createHttpAgent(configProvider());
185191
config.httpsAgent = agent;
186192
config.httpAgent = agent;
187193
config.proxy = false;
@@ -198,12 +204,16 @@ function setupInterceptors(
198204
);
199205
}
200206

201-
function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
207+
function addLoggingInterceptors(
208+
client: AxiosInstance,
209+
logger: Logger,
210+
configProvider: WorkspaceConfigurationProvider,
211+
) {
202212
client.interceptors.request.use(
203213
(config) => {
204214
const meta = createRequestMeta();
205215
(config as RequestConfigWithMeta).metadata = meta;
206-
logRequestStart(logger, meta.requestId, config);
216+
logRequest(logger, meta.requestId, config, getLogLevel(configProvider()));
207217
return config;
208218
},
209219
(error: unknown) => {
@@ -216,7 +226,7 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
216226
(response) => {
217227
const meta = (response.config as RequestConfigWithMeta).metadata;
218228
if (meta) {
219-
logRequestSuccess(logger, meta, response);
229+
logResponse(logger, meta, response, getLogLevel(configProvider()));
220230
}
221231
return response;
222232
},
@@ -226,3 +236,10 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
226236
},
227237
);
228238
}
239+
240+
function getLogLevel(cfg: WorkspaceConfiguration): HttpLogLevel {
241+
const logLevelStr = cfg
242+
.get("coder.httpClientLogLevel", HttpLogLevel[HttpLogLevel.BASIC])
243+
.toUpperCase();
244+
return HttpLogLevel[logLevelStr as keyof typeof HttpLogLevel];
245+
}

src/api/workspace.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,6 @@ export async function waitForBuild(
9696
logs,
9797
);
9898

99-
const closeHandler = () => {
100-
resolve();
101-
};
102-
10399
socket.addEventListener("message", (data) => {
104100
if (data.parseError) {
105101
writeEmitter.fire(
@@ -111,10 +107,6 @@ export async function waitForBuild(
111107
});
112108

113109
socket.addEventListener("error", (error) => {
114-
// Do not want to trigger the close handler and resolve the promise normally.
115-
socket.removeEventListener("close", closeHandler);
116-
socket.close();
117-
118110
const baseUrlRaw = client.getAxiosInstance().defaults.baseURL;
119111
return reject(
120112
new Error(
@@ -123,7 +115,7 @@ export async function waitForBuild(
123115
);
124116
});
125117

126-
socket.addEventListener("close", closeHandler);
118+
socket.addEventListener("close", () => resolve());
127119
});
128120

129121
writeEmitter.fire("Build complete\r\n");

src/commands.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,10 @@ export class Commands {
240240
token: string,
241241
isAutologin: boolean,
242242
): Promise<{ user: User; token: string } | null> {
243-
const cfg = vscode.workspace.getConfiguration();
244-
const client = CodeApi.create(url, token, this.storage.output, cfg);
245-
if (!needToken(cfg)) {
243+
const client = CodeApi.create(url, token, this.storage.output, () =>
244+
vscode.workspace.getConfiguration(),
245+
);
246+
if (!needToken(vscode.workspace.getConfiguration())) {
246247
try {
247248
const user = await client.getAuthenticatedUser();
248249
// For non-token auth, we write a blank token since the `vscodessh`

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
6666
url || "",
6767
await storage.getSessionToken(),
6868
storage.output,
69-
vscode.workspace.getConfiguration(),
69+
() => vscode.workspace.getConfiguration(),
7070
);
7171

7272
const myWorkspacesProvider = new WorkspaceProvider(

src/inbox.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
import * as vscode from "vscode";
66
import { CodeApi } from "./api/codeApi";
77
import { type Storage } from "./storage";
8-
import { OneWayCodeWebSocket } from "./websocket/oneWayCodeWebSocket";
8+
import { OneWayWebSocket } from "./websocket/oneWayWebSocket";
99

1010
// These are the template IDs of our notifications.
1111
// Maybe in the future we should avoid hardcoding
@@ -16,7 +16,7 @@ const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a";
1616
export class Inbox implements vscode.Disposable {
1717
readonly #storage: Storage;
1818
#disposed = false;
19-
#socket: OneWayCodeWebSocket<GetInboxNotificationResponse>;
19+
#socket: OneWayWebSocket<GetInboxNotificationResponse>;
2020

2121
constructor(workspace: Workspace, client: CodeApi, storage: Storage) {
2222
this.#storage = storage;

src/logging/formatters.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { InternalAxiosRequestConfig } from "axios";
2+
import prettyBytes from "pretty-bytes";
3+
4+
const SENSITIVE_HEADERS = ["Coder-Session-Token", "Proxy-Authorization"];
5+
6+
export function formatMethod(method: string | undefined): string {
7+
return (method ?? "GET").toUpperCase();
8+
}
9+
10+
export function formatContentLength(headers: Record<string, unknown>): string {
11+
const len = headers["content-length"];
12+
if (len && typeof len === "string") {
13+
const bytes = parseInt(len, 10);
14+
return isNaN(bytes) ? "(?b)" : `(${prettyBytes(bytes)})`;
15+
}
16+
return "(?b)";
17+
}
18+
19+
export function formatUri(
20+
config: InternalAxiosRequestConfig | undefined,
21+
): string {
22+
return config?.url || "<no url>";
23+
}
24+
25+
export function formatHeaders(headers: Record<string, unknown>): string {
26+
const formattedHeaders = Object.entries(headers)
27+
.map(([key, value]) => {
28+
if (SENSITIVE_HEADERS.includes(key)) {
29+
return `${key}: <redacted>`;
30+
}
31+
return `${key}: ${value}`;
32+
})
33+
.join("\n")
34+
.trim();
35+
36+
return formattedHeaders.length > 0 ? formattedHeaders : "<no headers>";
37+
}
38+
39+
export function formatBody(body: unknown): string {
40+
if (body) {
41+
return JSON.stringify(body);
42+
} else {
43+
return "<no body>";
44+
}
45+
}

0 commit comments

Comments
 (0)