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
21 changes: 17 additions & 4 deletions src/core/container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as vscode from "vscode";

import { AuthTelemetry } from "../instrumentation/auth";
import { SessionLogger } from "../logging/sessionLogger";
import { LoginCoordinator } from "../login/loginCoordinator";
import { OAuthCallback } from "../oauth/oauthCallback";
import { buildSession, extractExtensionVersion } from "../telemetry/event";
Expand All @@ -26,7 +27,9 @@ import type { Logger } from "../logging/logger";
* Centralizes the creation and management of all core services.
*/
export class ServiceContainer implements vscode.Disposable {
private readonly logger: vscode.LogOutputChannel;
private readonly outputChannel: vscode.LogOutputChannel;
private readonly sessionId: string;
private readonly logger: Logger;
private readonly pathResolver: PathResolver;
private readonly mementoManager: MementoManager;
private readonly secretsManager: SecretsManager;
Expand All @@ -42,7 +45,13 @@ export class ServiceContainer implements vscode.Disposable {
private readonly commandManager: CommandManager;

constructor(context: vscode.ExtensionContext) {
this.logger = vscode.window.createOutputChannel("Coder", { log: true });
this.outputChannel = vscode.window.createOutputChannel("Coder", {
log: true,
});
// One session ID per activation, shared by logs, API requests,
// telemetry, and the CLI so all data for a session correlates.
this.sessionId = newSessionId();
this.logger = new SessionLogger(this.outputChannel, this.sessionId);
this.pathResolver = new PathResolver(
context.globalStorageUri.fsPath,
context.logUri.fsPath,
Expand All @@ -56,7 +65,7 @@ export class ServiceContainer implements vscode.Disposable {

const session = buildSession(
extractExtensionVersion(context.extension.packageJSON),
newSessionId(),
this.sessionId,
);
const localJsonlSink = LocalJsonlSink.start(
{
Expand Down Expand Up @@ -139,6 +148,10 @@ export class ServiceContainer implements vscode.Disposable {
return this.logger;
}

getSessionId(): string {
return this.sessionId;
}
Comment on lines +151 to +153

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we expose this here? I don't think it's used


getCliManager(): CliManager {
return this.cliManager;
}
Expand Down Expand Up @@ -187,7 +200,7 @@ export class ServiceContainer implements vscode.Disposable {
try {
await this.telemetryService.dispose();
} finally {
this.logger.dispose();
this.outputChannel.dispose();
}
}
}
42 changes: 42 additions & 0 deletions src/logging/sessionLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Logger } from "./logger";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can actually simplify this using functional programming:

import type { Logger } from "./logger";

/**
 * Wraps a {@link Logger} so every message is prefixed, letting all lines that
 * share a prefix (a session ID, a workspace name) be found with one search.
 * Extra arguments are forwarded untouched.
 */
export function prefixLogger(inner: Logger, prefix: string): Logger {
	const tag = (message: string) => `${prefix} ${message}`;
	return {
		trace: (message, ...args) => inner.trace(tag(message), ...args),
		debug: (message, ...args) => inner.debug(tag(message), ...args),
		info: (message, ...args) => inner.info(tag(message), ...args),
		warn: (message, ...args) => inner.warn(tag(message), ...args),
		error: (message, ...args) => inner.error(tag(message), ...args),
		show: () => inner.show(),
	};
}

Maybe even call this prefixLogger.ts


/**
* Wraps a {@link Logger} and prefixes every message with the session ID so all
* log lines produced during a session can be correlated by searching for a
* single ID. Composition, not inheritance: it forwards to the underlying
* logger after tagging the message.
*/
export class SessionLogger implements Logger {
constructor(
private readonly inner: Logger,
private readonly sessionId: string,
) {}

private prefix(message: string): string {
return `[${this.sessionId}] ${message}`;
}
Comment on lines +15 to +17

@EhabY EhabY Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use shortId for the session ID? This is very verbose now and conflict is extremely unlikely to happen

Image

Also I'm not sure if this is clear that the first number is the session ID while the second number is the request ID


trace(message: string, ...args: unknown[]): void {
this.inner.trace(this.prefix(message), ...args);
}

debug(message: string, ...args: unknown[]): void {
this.inner.debug(this.prefix(message), ...args);
}

info(message: string, ...args: unknown[]): void {
this.inner.info(this.prefix(message), ...args);
}

warn(message: string, ...args: unknown[]): void {
this.inner.warn(this.prefix(message), ...args);
}

error(message: string, ...args: unknown[]): void {
this.inner.error(this.prefix(message), ...args);
}

show(): void {
this.inner.show();
}
}
45 changes: 45 additions & 0 deletions test/unit/logging/sessionLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";

import { SessionLogger } from "@/logging/sessionLogger";

import { createMockLogger } from "../../mocks/testHelpers";

const SESSION_ID = "0123456789abcdef0123456789abcdef";

describe("SessionLogger", () => {
it("prefixes every level with the session ID", () => {
const inner = createMockLogger();
const logger = new SessionLogger(inner, SESSION_ID);

logger.trace("trace msg");
logger.debug("debug msg");
logger.info("info msg");
logger.warn("warn msg");
logger.error("error msg");

expect(inner.trace).toHaveBeenCalledWith(`[${SESSION_ID}] trace msg`);
expect(inner.debug).toHaveBeenCalledWith(`[${SESSION_ID}] debug msg`);
expect(inner.info).toHaveBeenCalledWith(`[${SESSION_ID}] info msg`);
expect(inner.warn).toHaveBeenCalledWith(`[${SESSION_ID}] warn msg`);
expect(inner.error).toHaveBeenCalledWith(`[${SESSION_ID}] error msg`);
});

it("forwards additional arguments unchanged", () => {
const inner = createMockLogger();
const logger = new SessionLogger(inner, SESSION_ID);
const err = new Error("boom");

logger.error("failed", err, 42);

expect(inner.error).toHaveBeenCalledWith(`[${SESSION_ID}] failed`, err, 42);
});

it("delegates show() to the underlying logger", () => {
const inner = createMockLogger();
const logger = new SessionLogger(inner, SESSION_ID);

logger.show();

expect(inner.show).toHaveBeenCalledOnce();
});
});