Skip to content
Merged
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
28 changes: 20 additions & 8 deletions packages/amazonq/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as nls from 'vscode-nls'
import { LanguageClient, LanguageClientOptions, RequestType, State } from 'vscode-languageclient'
import { InlineCompletionManager } from '../app/inline/completion'
import { AmazonQLspAuth, encryptionKey, notificationTypes } from './auth'
import { RotatingLogChannel } from './rotatingLogChannel'
import {
CreateFilesParams,
DeleteFilesParams,
Expand Down Expand Up @@ -94,6 +95,23 @@ export async function startLanguageServer(

const clientId = 'amazonq'
const traceServerEnabled = Settings.instance.isSet(`${clientId}.trace.server`)

// Create custom output channel that writes to disk but sends UI output to the appropriate channel
const lspLogChannel = new RotatingLogChannel(
traceServerEnabled ? 'Amazon Q Language Server' : 'Amazon Q Logs',
extensionContext,
traceServerEnabled
? vscode.window.createOutputChannel('Amazon Q Language Server', { log: true })
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we are using Amazon Q Language Server in VSC? to align with Flare?

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link
Contributor Author

@abhraina-aws abhraina-aws Jul 17, 2025

Choose a reason for hiding this comment

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

This where the flare logs get printed they don't get printed through flare its in the toolkit. We are just trying to maintain the functionality here so that current std out experience doesn't change while enabling the logs on disk.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh I see if the tranceServerEnabled then we print logs to VSC
That make sense!
but this is existing right? Why do we need todo add this check again?

Copy link
Contributor Author

@abhraina-aws abhraina-aws Jul 17, 2025

Choose a reason for hiding this comment

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

Thats not how it works currently. We print all logs to VSC currently. If the setting is on, then we just take some of the logs to a different stdout for clean separation.

When interested in writing to the disk. I am trying to maintain the same behavior so that while std out separates based on the setting. The logs keep printing to the same file regardless. Thats why the check check moved to the top.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thats not how it works currently. We print all logs to VSC currently. If the setting is on, then we just take some of the logs to a different stdout for clean separation.

Can you point me to the logic, we have today? just for context but not a blocking for this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

packages/amazonq/src/lsp/client.ts

Sure! This file that we have open and commenting on is the logic that governs this. This is where we decide what to put where as far as LSP logs are concerned.

: globals.logOutputChannel
)

// Add cleanup for our file output channel
toDispose.push({
dispose: () => {
lspLogChannel.dispose()
},
})

let executable: string[] = []
// apply the GLIBC 2.28 path to node js runtime binary
if (isSageMaker()) {
Expand Down Expand Up @@ -191,15 +209,9 @@ export async function startLanguageServer(
},
},
/**
* When the trace server is enabled it outputs a ton of log messages so:
* When trace server is enabled, logs go to a seperate "Amazon Q Language Server" output.
* Otherwise, logs go to the regular "Amazon Q Logs" channel.
* Using our RotatingLogger for all logs
*/
...(traceServerEnabled
? {}
: {
outputChannel: globals.logOutputChannel,
}),
outputChannel: lspLogChannel,
}

const client = new LanguageClient(
Expand Down
228 changes: 228 additions & 0 deletions packages/amazonq/src/lsp/rotatingLogChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import * as vscode from 'vscode'
import * as path from 'path'
import * as fs from 'fs' // eslint-disable-line no-restricted-imports
import { getLogger } from 'aws-core-vscode/shared'

export class RotatingLogChannel implements vscode.LogOutputChannel {
private fileStream: fs.WriteStream | undefined
private originalChannel: vscode.LogOutputChannel
private logger = getLogger('amazonqLsp')
private _logLevel: vscode.LogLevel = vscode.LogLevel.Info
private currentFileSize = 0
// eslint-disable-next-line @typescript-eslint/naming-convention
private readonly MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
// eslint-disable-next-line @typescript-eslint/naming-convention
private readonly MAX_LOG_FILES = 4

constructor(
public readonly name: string,
private readonly extensionContext: vscode.ExtensionContext,
outputChannel: vscode.LogOutputChannel
) {
this.originalChannel = outputChannel
this.initFileStream()
}

private async cleanupOldLogs(): Promise<void> {
try {
const logDir = this.extensionContext.storageUri?.fsPath
if (!logDir) {
return
}

// Get all log files
const files = await fs.promises.readdir(logDir)
const logFiles = files
.filter((f) => f.startsWith('amazonq-lsp-') && f.endsWith('.log'))
.map((f) => ({
name: f,
path: path.join(logDir, f),
time: fs.statSync(path.join(logDir, f)).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time) // Sort newest to oldest

// Remove all but the most recent MAX_LOG_FILES files
for (const file of logFiles.slice(this.MAX_LOG_FILES - 1)) {
try {
await fs.promises.unlink(file.path)
this.logger.debug(`Removed old log file: ${file.path}`)
} catch (err) {
this.logger.error(`Failed to remove old log file ${file.path}: ${err}`)
}
}
} catch (err) {
this.logger.error(`Failed to cleanup old logs: ${err}`)
}
}

private getLogFilePath(): string {
const logDir = this.extensionContext.storageUri?.fsPath
if (!logDir) {
throw new Error('No storage URI available')
}

const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '-').replace('Z', '')
return path.join(logDir, `amazonq-lsp-${timestamp}.log`)
}

private async rotateLog(): Promise<void> {
try {
// Close current stream
if (this.fileStream) {
this.fileStream.end()
}

// Create new log file
const newLogPath = this.getLogFilePath()
this.fileStream = fs.createWriteStream(newLogPath, { flags: 'a' })
this.currentFileSize = 0

// Clean up old files
await this.cleanupOldLogs()

this.logger.info(`Created new log file: ${newLogPath}`)
} catch (err) {
this.logger.error(`Failed to rotate log file: ${err}`)
}
}

private initFileStream() {
try {
const logDir = this.extensionContext.storageUri
if (!logDir) {
this.logger.error('Failed to get storage URI for logs')
return
}

// Ensure directory exists
if (!fs.existsSync(logDir.fsPath)) {
fs.mkdirSync(logDir.fsPath, { recursive: true })
}

const logPath = this.getLogFilePath()
this.fileStream = fs.createWriteStream(logPath, { flags: 'a' })
this.currentFileSize = 0
this.logger.info(`Logging to file: ${logPath}`)
} catch (err) {
this.logger.error(`Failed to create log file: ${err}`)
}
}

get logLevel(): vscode.LogLevel {
return this._logLevel
}

get onDidChangeLogLevel(): vscode.Event<vscode.LogLevel> {
return this.originalChannel.onDidChangeLogLevel
}

trace(message: string, ...args: any[]): void {
this.originalChannel.trace(message, ...args)
this.writeToFile(`[TRACE] ${message}`)
}

debug(message: string, ...args: any[]): void {
this.originalChannel.debug(message, ...args)
this.writeToFile(`[DEBUG] ${message}`)
}

info(message: string, ...args: any[]): void {
this.originalChannel.info(message, ...args)
this.writeToFile(`[INFO] ${message}`)
}

warn(message: string, ...args: any[]): void {
this.originalChannel.warn(message, ...args)
this.writeToFile(`[WARN] ${message}`)
}

error(message: string | Error, ...args: any[]): void {
this.originalChannel.error(message, ...args)
this.writeToFile(`[ERROR] ${message instanceof Error ? message.stack || message.message : message}`)
}

append(value: string): void {
this.originalChannel.append(value)
this.writeToFile(value)
}

appendLine(value: string): void {
this.originalChannel.appendLine(value)
this.writeToFile(value + '\n')
}

replace(value: string): void {
this.originalChannel.replace(value)
this.writeToFile(`[REPLACE] ${value}`)
}

clear(): void {
this.originalChannel.clear()
}

show(preserveFocus?: boolean): void
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void
show(columnOrPreserveFocus?: vscode.ViewColumn | boolean, preserveFocus?: boolean): void {
if (typeof columnOrPreserveFocus === 'boolean') {
this.originalChannel.show(columnOrPreserveFocus)
} else {
this.originalChannel.show(columnOrPreserveFocus, preserveFocus)
}
}

hide(): void {
this.originalChannel.hide()
}

dispose(): void {
// First dispose the original channel
this.originalChannel.dispose()

// Close our file stream if it exists
if (this.fileStream) {
this.fileStream.end()
}

// Clean up all log files
const logDir = this.extensionContext.storageUri?.fsPath
if (logDir) {
try {
const files = fs.readdirSync(logDir)
for (const file of files) {
if (file.startsWith('amazonq-lsp-') && file.endsWith('.log')) {
fs.unlinkSync(path.join(logDir, file))
}
}
this.logger.info('Cleaned up all log files during disposal')
} catch (err) {
this.logger.error(`Failed to cleanup log files during disposal: ${err}`)
}
}
}

private writeToFile(content: string): void {
if (this.fileStream) {
try {
const timestamp = new Date().toISOString()
const logLine = `${timestamp} ${content}\n`
const size = Buffer.byteLength(logLine)

// If this write would exceed max file size, rotate first
if (this.currentFileSize + size > this.MAX_FILE_SIZE) {
void this.rotateLog()
}

this.fileStream.write(logLine)
this.currentFileSize += size
} catch (err) {
this.logger.error(`Failed to write to log file: ${err}`)
void this.rotateLog()
}
}
}
}
Loading
Loading