Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- Every `coder.*` command now records a `command.invoked` telemetry event with
its duration and outcome, so command latency and failures are captured
alongside other local telemetry.
- Local telemetry now records aggregated `http.requests` rollups for per-route
HTTP health without emitting one event per request.

### Fixed

Expand Down
23 changes: 21 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
]
},
"coder.telemetry.local": {
"markdownDescription": "Tunables for the local telemetry sink, which writes events as JSON Lines under the extension's global storage. Used when `#coder.telemetry.level#` is `local`. Missing or invalid fields fall back to defaults.",
"markdownDescription": "Advanced tunables for local telemetry collection. The local sink writes events as JSON Lines under the extension's global storage. Used when `#coder.telemetry.level#` is `local`. Missing or invalid fields fall back to defaults.",
"type": "object",
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -246,6 +246,22 @@
"minimum": 4096,
"default": 104857600,
"markdownDescription": "Cap, in bytes, on the combined size of telemetry files. Oldest files are deleted on activation until the total is under the cap."
},
"httpRequests": {
"type": "object",
"additionalProperties": false,
"markdownDescription": "Tunables for HTTP request rollup telemetry.",
"properties": {
"windowSeconds": {
"type": "number",
"minimum": 1,
"default": 60,
"markdownDescription": "Tumbling window length, in seconds, for aggregated `http.requests` events."
}
},
"default": {
"windowSeconds": 60
}
}
},
"default": {
Expand All @@ -254,7 +270,10 @@
"bufferLimit": 500,
"maxFileBytes": 5242880,
"maxAgeDays": 30,
"maxTotalBytes": 104857600
"maxTotalBytes": 104857600,
"httpRequests": {
"windowSeconds": 60
}
},
"tags": [
"telemetry"
Expand Down
134 changes: 108 additions & 26 deletions src/api/coderApi.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,12 @@
import {
type AxiosResponseHeaders,
type AxiosInstance,
isAxiosError,
type AxiosHeaders,
type AxiosInstance,
type AxiosResponseHeaders,
type AxiosResponseTransformer,
isAxiosError,
} from "axios";
import { Api } from "coder/site/src/api/api";
import {
type ServerSentEvent,
type GetInboxNotificationResponse,
type ProvisionerJobLog,
type Workspace,
type WorkspaceAgent,
type WorkspaceAgentLog,
} from "coder/site/src/api/typesGenerated";
import * as vscode from "vscode";
import { type ClientOptions } from "ws";

import { watchConfigurationChanges } from "../configWatcher";
import { ClientCertificateError } from "../error/clientCertificateError";
Expand All @@ -25,23 +16,31 @@ import { getHeaders } from "../headers";
import { EventStreamLogger } from "../logging/eventStreamLogger";
import {
createRequestMeta,
logRequest,
logError,
logRequest,
logResponse,
} from "../logging/httpLogger";
import { type Logger } from "../logging/logger";
import {
type RequestConfigWithMeta,
HttpRequestsTelemetry,
NOOP_HTTP_REQUESTS_TELEMETRY,
type HttpRequestsTelemetryRecorder,
} from "../logging/httpRequestsTelemetry";
import {
HttpClientLogLevel,
type RequestConfigWithMeta,
} from "../logging/types";
import { sizeOf } from "../logging/utils";
import { getHeaderCommand } from "../settings/headers";
import { HttpStatusCode, WebSocketCloseCode } from "../websocket/codes";
import {
type UnidirectionalStream,
type CloseEvent,
type ErrorEvent,
} from "../websocket/eventStreamConnection";
LOCAL_TELEMETRY_SETTING,
readHttpRequestsTelemetryConfig,
type HttpRequestsTelemetryConfig,
} from "../settings/telemetry";
import {
NOOP_TELEMETRY_REPORTER,
type TelemetryReporter,
} from "../telemetry/reporter";
import { HttpStatusCode, WebSocketCloseCode } from "../websocket/codes";
import {
OneWayWebSocket,
type OneWayWebSocketInit,
Expand All @@ -56,8 +55,29 @@ import { SseConnection } from "../websocket/sseConnection";
import { getRefreshCommand, refreshCertificates } from "./certificateRefresh";
import { createHttpAgent } from "./utils";

import type {
GetInboxNotificationResponse,
ProvisionerJobLog,
ServerSentEvent,
Workspace,
WorkspaceAgent,
WorkspaceAgentLog,
} from "coder/site/src/api/typesGenerated";
import type { ClientOptions } from "ws";

import type { Logger } from "../logging/logger";
import type {
CloseEvent,
ErrorEvent,
UnidirectionalStream,
} from "../websocket/eventStreamConnection";

const coderSessionTokenHeader = "Coder-Session-Token";

const NOOP_DISPOSABLE: vscode.Disposable = {
dispose: () => undefined,
};

/**
* Configuration settings that affect WebSocket connections.
* Changes to these settings will trigger WebSocket reconnection.
Expand Down Expand Up @@ -85,10 +105,15 @@ export class CoderApi extends Api implements vscode.Disposable {
ReconnectingWebSocket<never>
>();
private readonly configWatcher: vscode.Disposable;
private readonly httpRequestsConfigWatcher: vscode.Disposable;

private constructor(private readonly output: Logger) {
private constructor(
private readonly output: Logger,
private readonly httpRequestsTelemetry: HttpRequestsTelemetryRecorder,
) {
super();
this.configWatcher = this.watchConfigChanges();
this.httpRequestsConfigWatcher = this.watchHttpRequestsConfigChanges();
}

/**
Expand All @@ -99,11 +124,13 @@ export class CoderApi extends Api implements vscode.Disposable {
baseUrl: string,
token: string | undefined,
output: Logger,
telemetry: TelemetryReporter = NOOP_TELEMETRY_REPORTER,
): CoderApi {
const client = new CoderApi(output);
const httpRequestsTelemetry = createHttpRequestsTelemetry(telemetry);
const client = new CoderApi(output, httpRequestsTelemetry);
client.setCredentials(baseUrl, token);

setupInterceptors(client, output);
setupInterceptors(client, output, httpRequestsTelemetry);
return client;
}

Expand Down Expand Up @@ -155,6 +182,8 @@ export class CoderApi extends Api implements vscode.Disposable {
*/
dispose(): void {
this.configWatcher.dispose();
this.httpRequestsConfigWatcher.dispose();
this.httpRequestsTelemetry.dispose();
for (const socket of this.reconnectingSockets) {
socket.close();
}
Expand Down Expand Up @@ -187,6 +216,32 @@ export class CoderApi extends Api implements vscode.Disposable {
});
}

private watchHttpRequestsConfigChanges(): vscode.Disposable {
if (this.httpRequestsTelemetry === NOOP_HTTP_REQUESTS_TELEMETRY) {
return NOOP_DISPOSABLE;
}

return watchConfigurationChanges(
[
{
setting: LOCAL_TELEMETRY_SETTING,
getValue: () =>
readHttpRequestsTelemetryConfig(
vscode.workspace.getConfiguration(),
),
},
],
(changes) => {
const config = changes.get(LOCAL_TELEMETRY_SETTING) as
| HttpRequestsTelemetryConfig
| undefined;
if (config) {
this.httpRequestsTelemetry.updateConfig(config);
}
},
);
}

watchInboxNotifications = async (
watchTemplates: string[],
watchTargets: string[],
Expand Down Expand Up @@ -473,8 +528,29 @@ export class CoderApi extends Api implements vscode.Disposable {
/**
* Set up logging and request interceptors for the CoderApi instance.
*/
function setupInterceptors(client: CoderApi, output: Logger): void {
addLoggingInterceptors(client.getAxiosInstance(), output);
function createHttpRequestsTelemetry(
telemetry: TelemetryReporter,
): HttpRequestsTelemetryRecorder {
if (telemetry === NOOP_TELEMETRY_REPORTER) {
return NOOP_HTTP_REQUESTS_TELEMETRY;
}

return new HttpRequestsTelemetry(
telemetry,
readHttpRequestsTelemetryConfig(vscode.workspace.getConfiguration()),
);
}

function setupInterceptors(
client: CoderApi,
output: Logger,
httpRequestsTelemetry: HttpRequestsTelemetryRecorder,
): void {
addLoggingInterceptors(
client.getAxiosInstance(),
output,
httpRequestsTelemetry,
);

client.getAxiosInstance().interceptors.request.use(async (config) => {
const baseUrl = client.getAxiosInstance().defaults.baseURL;
Expand Down Expand Up @@ -522,7 +598,11 @@ function setupInterceptors(client: CoderApi, output: Logger): void {
);
}

function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {
function addLoggingInterceptors(
client: AxiosInstance,
logger: Logger,
httpRequestsTelemetry: HttpRequestsTelemetryRecorder,
) {
client.interceptors.request.use(
(config) => {
const configWithMeta = config as RequestConfigWithMeta;
Expand Down Expand Up @@ -555,10 +635,12 @@ function addLoggingInterceptors(client: AxiosInstance, logger: Logger) {

client.interceptors.response.use(
(response) => {
httpRequestsTelemetry.recordResponse(response);
logResponse(logger, response, getLogLevel());
return response;
},
(error: unknown) => {
httpRequestsTelemetry.recordError(error);
logError(logger, error, getLogLevel());
throw error;
},
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
const secretsManager = serviceContainer.getSecretsManager();
const contextManager = serviceContainer.getContextManager();
const commandManager = serviceContainer.getCommandManager();
const telemetryService = serviceContainer.getTelemetryService();

// Migrate auth storage from old flat format to new label-based format
await migrateAuthStorage(serviceContainer);
Expand Down Expand Up @@ -114,6 +115,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
(await secretsManager.getSessionAuth(deployment?.safeHostname ?? ""))
?.token,
output,
telemetryService,
);
ctx.subscriptions.push(client);

Expand Down
Loading
Loading