Path: blob/main/extensions/copilot/src/platform/log/common/logService.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { createServiceIdentifier } from '../../../util/common/services';6import { Disposable } from '../../../util/vs/base/common/lifecycle';78export const ILogService = createServiceIdentifier<ILogService>('ILogService');910/**11* Log levels (taken from vscode.d.ts)12*/13export enum LogLevel {1415/**16* No messages are logged with this level.17*/18Off = 0,1920/**21* All messages are logged with this level.22*/23Trace = 1,2425/**26* Messages with debug and higher log level are logged with this level.27*/28Debug = 2,2930/**31* Messages with info and higher log level are logged with this level.32*/33Info = 3,3435/**36* Messages with warning and higher log level are logged with this level.37*/38Warning = 4,3940/**41* Only error messages are logged with this level.42*/43Error = 544}4546export interface ILogTarget {47logIt(level: LogLevel, metadataStr: string, ...extra: any[]): void;48show?(preserveFocus?: boolean): void;49}5051/**52* Utility functions for creating ILogTarget instances.53*/54export namespace LogTarget {55/**56* Creates an ILogTarget from a simple callback function.57*58* @example59* logger.withExtraTarget(LogTarget.fromCallback((level, msg) => {60* console.log(`[${LogLevel[level]}] ${msg}`);61* }));62*/63export function fromCallback(fn: (level: LogLevel, message: string) => void): ILogTarget {64return { logIt: fn };65}66}6768// Simple implementation of a log targe used for logging to the console.69export class ConsoleLog implements ILogTarget {70constructor(private readonly prefix?: string, private readonly minLogLevel: LogLevel = LogLevel.Warning) { }7172logIt(level: LogLevel, metadataStr: string, ...extra: any[]) {73if (this.prefix) {74metadataStr = `${this.prefix}${metadataStr}`;75}7677// Note we don't log INFO or DEBUG messages into console.78// They are still logged in the output channel.79if (level === LogLevel.Error) {80console.error(metadataStr, ...extra);81} else if (level === LogLevel.Warning) {82console.warn(metadataStr, ...extra);83} else if (level >= this.minLogLevel) {84console.log(metadataStr, ...extra);85}86}87}8889export interface ILogService extends ILogger {90readonly _serviceBrand: undefined;91}9293/**94* Mirrors vscode's {@link LogOutputChannel} in terms of available logging functions95* Args has been ommitted for now in favor of simplifying the interface96*/97export interface ILogger {98trace(message: string): void;99debug(message: string): void;100info(message: string): void;101warn(message: string): void;102/**103* Logs an error message. Prefer this method over `error()` when logging exception details.104*105* @param error The Error object that was thrown106* @param message An optional message for context (e.g. "Request error"). Must not contain customer data. **Do not include stack trace or messages from the error object.**107*/108error(error: string | Error, message?: string): void;109show(preserveFocus?: boolean): void;110111/**112* Creates a sub-logger with a topic prefix. All messages logged through113* the sub-logger will be prefixed with the topic, e.g., `[Topic] message`.114*115* Sub-loggers can be nested, and the prefixes will accumulate,116* e.g., `[Parent][Child] message`.117*118* Sub-loggers inherit extra targets from their parent.119*120* @param topic The topic name or array of topic names to prefix messages with121*/122createSubLogger(topic: string | readonly string[]): ILogger;123124/**125* Returns a new logger that also logs to the specified extra target.126* The original logger is unchanged (immutable).127*128* Can be chained to add multiple targets. Sub-loggers created from this129* logger will inherit all extra targets.130*131* Errors thrown by extra targets are silently caught.132*133* @param target An ILogTarget instance134* @returns A new ILogger with the extra target attached135*136* @example137* const logger = logService138* .createSubLogger('MyFeature')139* .withExtraTarget(LogTarget.fromCallback((level, msg) => {140* logContext.trace(msg);141* }));142*/143withExtraTarget(target: ILogTarget): ILogger;144}145146export class LogServiceImpl extends Disposable implements ILogService {147declare _serviceBrand: undefined;148149readonly logger: LoggerImpl;150151constructor(152logTargets: ILogTarget[],153) {154super();155this.logger = new LoggerImpl(logTargets);156}157158// Delegate logging methods directly to the internal logger159trace(message: string): void {160this.logger.trace(message);161}162163debug(message: string): void {164this.logger.debug(message);165}166167info(message: string): void {168this.logger.info(message);169}170171warn(message: string): void {172this.logger.warn(message);173}174175error(error: string | Error, message?: string): void {176this.logger.error(error, message);177}178179show(preserveFocus?: boolean): void {180this.logger.show(preserveFocus);181}182183createSubLogger(topic: string | readonly string[]): ILogger {184return this.logger.createSubLogger(topic);185}186187withExtraTarget(target: ILogTarget): ILogger {188return this.logger.withExtraTarget(target);189}190}191192class LoggerImpl implements ILogger {193constructor(194private readonly _logTargets: ILogTarget[],195) { }196197private _logIt(level: LogLevel, message: string): void {198LogMemory.addLog(LogLevel[level], message);199this._logTargets.forEach(t => t.logIt(level, message));200}201202trace(message: string): void {203this._logIt(LogLevel.Trace, message);204}205206debug(message: string): void {207this._logIt(LogLevel.Debug, message);208}209210info(message: string): void {211this._logIt(LogLevel.Info, message);212}213214warn(message: string): void {215this._logIt(LogLevel.Warning, message);216}217218error(error: string | Error, message?: string): void {219this._logIt(LogLevel.Error, collectErrorMessages(error) + (message ? `: ${message}` : ''));220}221222show(preserveFocus?: boolean): void {223this._logTargets.forEach(t => t.show?.(preserveFocus));224}225226createSubLogger(topic: string | readonly string[]): ILogger {227return new SubLogger(this, topic);228}229230withExtraTarget(target: ILogTarget): ILogger {231return new LoggerWithExtraTargets(this, [target]);232}233}234235class SubLogger implements ILogger {236private readonly _prefix: string;237238constructor(239private readonly _parent: ILogger,240topic: string | readonly string[],241existingPrefix?: string,242) {243const topics = Array.isArray(topic) ? topic : [topic];244const newPrefix = topics.map(t => `[${t}]`).join('');245this._prefix = existingPrefix ? existingPrefix + newPrefix : newPrefix;246}247248private _prefixMessage(message: string): string {249return `${this._prefix} ${message}`;250}251252trace(message: string): void {253this._parent.trace(this._prefixMessage(message));254}255256debug(message: string): void {257this._parent.debug(this._prefixMessage(message));258}259260info(message: string): void {261this._parent.info(this._prefixMessage(message));262}263264warn(message: string): void {265this._parent.warn(this._prefixMessage(message));266}267268error(error: string | Error, message?: string): void {269const prefixedMessage = message ? this._prefixMessage(message) : this._prefix;270this._parent.error(error, prefixedMessage);271}272273show(preserveFocus?: boolean): void {274this._parent.show(preserveFocus);275}276277createSubLogger(topic: string | readonly string[]): ILogger {278return new SubLogger(this._parent, topic, this._prefix);279}280281withExtraTarget(target: ILogTarget): ILogger {282return new LoggerWithExtraTargets(this, [target], this._prefix);283}284}285286class LoggerWithExtraTargets implements ILogger {287constructor(288private readonly _parent: ILogger,289private readonly _extraTargets: readonly ILogTarget[],290private readonly _prefix: string = '',291) { }292293private _notifyExtraTargets(level: LogLevel, message: string): void {294const prefixedMessage = this._prefix ? `${this._prefix} ${message}` : message;295for (const target of this._extraTargets) {296try {297target.logIt(level, prefixedMessage);298} catch {299// Silent catch - extra targets must not affect primary logging300}301}302}303304trace(message: string): void {305this._notifyExtraTargets(LogLevel.Trace, message);306this._parent.trace(message);307}308309debug(message: string): void {310this._notifyExtraTargets(LogLevel.Debug, message);311this._parent.debug(message);312}313314info(message: string): void {315this._notifyExtraTargets(LogLevel.Info, message);316this._parent.info(message);317}318319warn(message: string): void {320this._notifyExtraTargets(LogLevel.Warning, message);321this._parent.warn(message);322}323324error(error: string | Error, message?: string): void {325// For extra targets, format a simple message326const errorStr = typeof error === 'string' ? error : (error.message || 'Error');327const fullMessage = message ? `${errorStr}: ${message}` : errorStr;328this._notifyExtraTargets(LogLevel.Error, fullMessage);329this._parent.error(error, message);330}331332show(preserveFocus?: boolean): void {333this._parent.show(preserveFocus);334for (const target of this._extraTargets) {335try {336target.show?.(preserveFocus);337} catch {338// Silent catch339}340}341}342343createSubLogger(topic: string | readonly string[]): ILogger {344// Sub-logger inherits extra targets with updated prefix345const topics = Array.isArray(topic) ? topic : [topic];346const newPrefix = this._prefix + topics.map(t => `[${t}]`).join('');347return new LoggerWithExtraTargets(348this._parent.createSubLogger(topic),349this._extraTargets,350newPrefix351);352}353354withExtraTarget(target: ILogTarget): ILogger {355return new LoggerWithExtraTargets(356this._parent,357[...this._extraTargets, target],358this._prefix359);360}361}362363export function collectErrorMessages(e: any): string {364// Collect error messages from nested errors as seen with Node's `fetch`.365const seen = new Set<any>();366function collect(e: any, indent: string): string {367if (!e || !['object', 'string'].includes(typeof e) || seen.has(e)) {368return '';369}370seen.add(e);371const message = typeof e === 'string' ? e : (e.stack || e.message || e.code || '');372const messageStr = message.toString?.() as (string | undefined) || '';373return [374messageStr ? `${messageStr.split('\n').map(line => `${indent}${line}`).join('\n')}\n` : '',375e.chromiumDetails ? `${indent}${JSON.stringify(extractChromiumDetails(e.chromiumDetails))}\n` : '',376collect(e.cause, indent + ' '),377...(Array.isArray(e.errors) ? e.errors.map((e: any) => collect(e, indent + ' ')) : []),378].join('');379}380return collect(e, '')381.trim();382}383384export function collectSingleLineErrorMessage(e: any, includeDetails = false): string {385// Collect error messages from nested errors as seen with Node's `fetch`.386const seen = new Set<any>();387function collect(e: any): string {388if (!e || !['object', 'string'].includes(typeof e) || seen.has(e)) {389return '';390}391seen.add(e);392const message = typeof e === 'string' ? e : (e.message || e.code || '');393const messageStr = message.toString?.() as (string | undefined) || '';394const messageLine = messageStr.trim().split('\n').join(' ');395const details = [396...(includeDetails && e.chromiumDetails ? [JSON.stringify(extractChromiumDetails(e.chromiumDetails))] : []),397...(e.cause ? [collect(e.cause)] : []),398...(Array.isArray(e.errors) ? e.errors.map((e: any) => collect(e)) : []),399].join(', ');400return details ? `${messageLine}: ${details}` : messageLine;401}402return collect(e);403}404405/**406* Sanitizes a network error message for telemetry by replacing hostnames,407* IP addresses, and credentials with placeholders.408*/409export function sanitizeNetworkErrorForTelemetry(message: string): string {410// Strip credentials and host from proxy result strings (e.g., "PROXY user:pass@host" → "PROXY <credentials>@<host>")411message = message.replace(/(\b(?:PROXY|HTTPS?|SOCKS[45]?)\s+)[^\s]+@([^\s:\/]+)/gi, '$1<credentials>@<host>');412// Strip host from proxy result strings without credentials (e.g., "PROXY host:8080" → "PROXY <host>:8080")413message = message.replace(/(\b(?:PROXY|HTTPS?|SOCKS[45]?)\s+)([a-zA-Z0-9][-a-zA-Z0-9.]*)/gi, '$1<host>');414// Strip credentials and host from URLs (e.g., "://user:pass@host" → "://<credentials>@<host>")415message = message.replace(/(\/\/)[^\s/]+@([^\s:\/]+)/g, '$1<credentials>@<host>');416// Replace IPv4 addresses, preserving the port if present417message = message.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<ip>');418// Replace IPv6 addresses (full form, e.g., "2001:db8:85a3:0:0:8a2e:370:7334")419message = message.replace(/(?<![a-zA-Z_:])(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}(?![a-zA-Z_])/g, '<ip>');420// Replace IPv6 addresses (compressed form with ::, e.g., "2001:db8::1" or "::1")421message = message.replace(/(?<![a-zA-Z_:])(?:(?:[0-9a-fA-F]{1,4}:){1,7}|:):[0-9a-fA-F:]*[0-9a-fA-F](?![a-zA-Z_])/g, '<ip>');422// Replace FQDNs (at least one dot, TLD of 2+ alpha chars), preserving the port if present423message = message.replace(/\b([a-zA-Z0-9][-a-zA-Z0-9]*\.)+[a-zA-Z]{2,}\b/g, '<host>');424return message;425}426427/**428* Chromium error details attached by Electron to fetch errors.429* Electron's network service process runs separately; when it crashes,430* `network_process_crashed` is set to `true` on the error's `chromiumDetails`.431*/432export interface ElectronFetchErrorChromiumDetails {433readonly is_request_error?: boolean;434readonly network_process_crashed?: boolean;435readonly session_state?: any;436readonly drain_error?: any;437readonly drain_description?: any;438readonly go_away_error?: any;439readonly go_away_error_details?: any;440readonly go_away_debug_data?: any;441readonly rst_stream_error?: any;442readonly rst_stream_error_details?: any;443readonly rst_stream_description?: any;444readonly last_framer_error?: any;445readonly last_framer_error_details?: any;446readonly error_source?: any;447readonly aliases?: any;448readonly proxy?: any;449readonly in_flight_write?: any;450readonly buffered_spdy_framer?: any;451readonly tls_info?: any;452readonly socket_info?: any;453readonly url_loader_error?: any;454readonly active_stream_details?: any;455readonly closed_stream_details?: any;456}457458function extractChromiumDetails(details: ElectronFetchErrorChromiumDetails): any {459if (!details || typeof details !== 'object') {460return {};461}462463if (details.is_request_error !== undefined && details.session_state === undefined) {464return {465is_request_error: details.is_request_error,466network_process_crashed: details.network_process_crashed,467};468}469470const extracted: any = {471drain_error: details.drain_error,472drain_description: details.drain_description,473go_away_error: details.go_away_error,474go_away_error_details: details.go_away_error_details,475go_away_debug_data: details.go_away_debug_data,476rst_stream_error: details.rst_stream_error,477rst_stream_error_details: details.rst_stream_error_details,478rst_stream_description: details.rst_stream_description,479last_framer_error: details.last_framer_error,480last_framer_error_details: details.last_framer_error_details,481error_source: details.error_source,482aliases_length: Array.isArray(details.aliases) ? details.aliases.length : undefined,483};484485if (details.proxy) {486const proxyString = String(details.proxy);487const proxySchemes = [...proxyString.matchAll(/([a-z][a-z0-9+.-]*):\/\//gi)].map(match => match[1]);488if (proxySchemes.length > 0) {489extracted.proxy_schemes = proxySchemes;490}491}492493if (details.in_flight_write && typeof details.in_flight_write === 'object') {494extracted.in_flight_write = {495frame_type: details.in_flight_write.frame_type,496frame_size: details.in_flight_write.frame_size,497remaining_size: details.in_flight_write.remaining_size,498};499}500501if (details.buffered_spdy_framer && typeof details.buffered_spdy_framer === 'object') {502extracted.buffered_spdy_framer = {503frames_received: details.buffered_spdy_framer.frames_received,504has_error: details.buffered_spdy_framer.has_error,505message_fully_read: details.buffered_spdy_framer.message_fully_read,506};507}508509if (details.session_state && typeof details.session_state === 'object') {510const state = details.session_state;511extracted.session_state = {512availability_state: state.availability_state,513session_send_window: state.session_send_window,514session_recv_window: state.session_recv_window,515stream_initial_send_window: state.stream_initial_send_window,516stream_initial_recv_window: state.stream_initial_recv_window,517send_stalled_by_session_window: state.send_stalled_by_session_window,518active_stream_count: state.active_stream_count,519created_stream_count: state.created_stream_count,520max_concurrent_streams: state.max_concurrent_streams,521highest_stream_id_sent: state.highest_stream_id_sent,522frames_sent: state.frames_sent,523frames_received: state.frames_received,524ping_in_flight: state.ping_in_flight,525last_ping_sent_ms: state.last_ping_sent_ms,526next_ping_id: state.next_ping_id,527failed_ping_count: state.failed_ping_count,528support_websocket: state.support_websocket,529deprecate_http2_priorities: state.deprecate_http2_priorities,530streams_initiated_count: state.streams_initiated_count,531streams_abandoned_count: state.streams_abandoned_count,532read_state: state.read_state,533write_state: state.write_state,534pending_create_stream_request_count: state.pending_create_stream_request_count,535error: state.error,536error_on_unavailable: state.error_on_unavailable,537unacked_recv_window_bytes: state.unacked_recv_window_bytes,538last_good_stream_id: state.last_good_stream_id,539debug_stream_id: state.debug_stream_id,540has_ping_based_connection_checking: state.has_ping_based_connection_checking,541num_broken_connection_detection_requests: state.num_broken_connection_detection_requests,542session_max_queued_capped_frames: state.session_max_queued_capped_frames,543num_queued_capped_frames: state.num_queued_capped_frames,544check_ping_status_pending: state.check_ping_status_pending,545in_confirm_handshake: state.in_confirm_handshake,546http2_end_stream_with_data_frame: state.http2_end_stream_with_data_frame,547reused: state.reused,548session_max_recv_window_size: state.session_max_recv_window_size,549max_header_table_size: state.max_header_table_size,550time_since_last_read_ms: state.time_since_last_read_ms,551time_since_last_write_ms: state.time_since_last_write_ms,552time_since_last_recv_window_update_ms: state.time_since_last_recv_window_update_ms,553};554}555556if (details.tls_info && typeof details.tls_info === 'object') {557const tls = details.tls_info;558extracted.tls_info = {559is_secure_connection: tls.is_secure_connection,560ssl_version: tls.ssl_version,561cipher_suite: tls.cipher_suite,562negotiated_alpn: tls.negotiated_alpn,563cert_status: tls.cert_status,564is_issued_by_known_root: tls.is_issued_by_known_root,565handshake_type: tls.handshake_type,566client_cert_sent: tls.client_cert_sent,567exchange_group: tls.key_exchange_group,568ct_compliance: tls.ct_compliance,569alps_negotiated: tls.alps_negotiated,570};571}572573if (details.socket_info && typeof details.socket_info === 'object') {574const socket = details.socket_info;575extracted.socket_info = {576is_connected: socket.is_connected,577was_ever_used: socket.was_ever_used,578dns_lookup_duration_ms: socket.dns_lookup_duration_ms,579tcp_connect_duration_ms: socket.tcp_connect_duration_ms,580ssl_handshake_duration_ms: socket.ssl_handshake_duration_ms,581owned_socket: socket.owned_socket,582socket_reuse_type: socket.socket_reuse_type,583};584}585586if (details.url_loader_error && typeof details.url_loader_error === 'object') {587extracted.url_loader_error = {588is_request_error: details.url_loader_error.is_request_error,589network_process_crashed: details.url_loader_error.network_process_crashed,590};591}592593if (Array.isArray(details.active_stream_details)) {594extracted.active_stream_details = details.active_stream_details.map((stream: any) => ({595stream_id: stream.stream_id,596io_state: stream.io_state,597type: stream.type,598priority: stream.priority,599send_window_size: stream.send_window_size,600recv_window_size: stream.recv_window_size,601max_recv_window_size: stream.max_recv_window_size,602unacked_recv_window_bytes: stream.unacked_recv_window_bytes,603send_stalled_by_flow_control: stream.send_stalled_by_flow_control,604raw_sent_bytes: stream.raw_sent_bytes,605raw_received_bytes: stream.raw_received_bytes,606recv_bytes: stream.recv_bytes,607pending_send_status: stream.pending_send_status,608response_state: stream.response_state,609pending_send_data_remaining: stream.pending_send_data_remaining,610request_time_ms: stream.request_time_ms,611response_time_ms: stream.response_time_ms,612}));613}614615if (Array.isArray(details.closed_stream_details)) {616extracted.closed_stream_details = details.closed_stream_details.map((stream: any) => ({617stream_id: stream.stream_id,618io_state: stream.io_state,619type: stream.type,620priority: stream.priority,621send_window_size: stream.send_window_size,622recv_window_size: stream.recv_window_size,623max_recv_window_size: stream.max_recv_window_size,624unacked_recv_window_bytes: stream.unacked_recv_window_bytes,625send_stalled_by_flow_control: stream.send_stalled_by_flow_control,626raw_sent_bytes: stream.raw_sent_bytes,627raw_received_bytes: stream.raw_received_bytes,628recv_bytes: stream.recv_bytes,629pending_send_status: stream.pending_send_status,630response_state: stream.response_state,631pending_send_data_remaining: stream.pending_send_data_remaining,632request_time_ms: stream.request_time_ms,633response_time_ms: stream.response_time_ms,634}));635}636637return extracted;638}639640export class LogMemory {641private static _logs: string[] = [];642private static _requestIds: string[] = [];643private static readonly MAX_LOGS = 50;644645/**646* Extracts the requestId from a log message if it matches the expected pattern.647* Returns a string in the format 'requestId: {string}' or undefined if not found.648*/649private static extractRequestIdFromMessage(message: string): string | undefined {650const match = message.match(/request done: requestId: \[([0-9a-fA-F-]+)\] model deployment ID: \[/);651if (match) {652const requestId = match[1];653if (!this._requestIds.includes(requestId)) {654return requestId;655}656}657return undefined;658}659660static addLog(level: string, message: string): void {661if (this._logs.length >= this.MAX_LOGS) {662this._logs.shift();663}664this._logs.push(`${level}: ${message}`);665666// Extract and store requestId if present667if (this._requestIds.length >= this.MAX_LOGS) {668this._requestIds.shift();669}670const requestId = this.extractRequestIdFromMessage(message);671if (requestId) {672this._requestIds.push(requestId);673}674}675676static getLogs(): string[] {677return this._logs;678}679680static getRequestIds(): string[] {681return this._requestIds;682}683}684685686