diff --git a/core/src/common.ts b/core/src/common.ts index 0a63e30..435618e 100644 --- a/core/src/common.ts +++ b/core/src/common.ts @@ -130,7 +130,8 @@ export {GOOGLE_SEARCH, GoogleSearchTool} from './tools/google_search_tool.js'; export {LongRunningFunctionTool} from './tools/long_running_tool.js'; export {ToolConfirmation} from './tools/tool_confirmation.js'; export {ToolContext} from './tools/tool_context.js'; -export {LogLevel, setLogLevel} from './utils/logger.js'; +export {LogLevel, getLogger, setLogLevel, setLogger} from './utils/logger.js'; +export type {Logger} from './utils/logger.js'; export {isGemini2OrAbove} from './utils/model_name.js'; export {zodObjectToSchema} from './utils/simple_zod_to_json.js'; export {GoogleLLMVariant} from './utils/variant_utils.js'; diff --git a/core/src/utils/logger.ts b/core/src/utils/logger.ts index 4581153..02a379a 100644 --- a/core/src/utils/logger.ts +++ b/core/src/utils/logger.ts @@ -96,6 +96,17 @@ class SimpleLogger implements Logger { } } +/** + * A no-op logger that discards all log messages. + */ +class NoOpLogger implements Logger { + log(_level: LogLevel, ..._args: unknown[]): void {} + debug(..._args: unknown[]): void {} + info(..._args: unknown[]): void {} + warn(..._args: unknown[]): void {} + error(..._args: unknown[]): void {} +} + const LOG_LEVEL_STR: Record = { [LogLevel.DEBUG]: 'DEBUG', [LogLevel.INFO]: 'INFO', @@ -116,7 +127,46 @@ function getColoredPrefix(level: LogLevel): string { return `${CONSOLE_COLOR_MAP[level]}[ADK ${LOG_LEVEL_STR[level]}]:${RESET_COLOR}`; } +let currentLogger: Logger = new SimpleLogger(); + +/** + * Sets a custom logger for ADK, or null to disable logging. + */ +export function setLogger(customLogger: Logger | null): void { + currentLogger = customLogger ?? new NoOpLogger(); +} + +/** + * Gets the current logger instance. + */ +export function getLogger(): Logger { + return currentLogger; +} + +/** + * Resets the logger to the default SimpleLogger. + */ +export function resetLogger(): void { + currentLogger = new SimpleLogger(); +} + /** * The logger instance for ADK. */ -export const logger = new SimpleLogger(); +export const logger: Logger = { + log(level: LogLevel, ...args: unknown[]): void { + currentLogger.log(level, ...args); + }, + debug(...args: unknown[]): void { + currentLogger.debug(...args); + }, + info(...args: unknown[]): void { + currentLogger.info(...args); + }, + warn(...args: unknown[]): void { + currentLogger.warn(...args); + }, + error(...args: unknown[]): void { + currentLogger.error(...args); + }, +}; diff --git a/core/test/utils/logger_test.ts b/core/test/utils/logger_test.ts new file mode 100644 index 0000000..8f0e3f8 --- /dev/null +++ b/core/test/utils/logger_test.ts @@ -0,0 +1,149 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {getLogger, Logger, LogLevel, setLogger, setLogLevel} from '@google/adk'; + +import {resetLogger} from '../../src/utils/logger.js'; + +describe('setLogger', () => { + beforeEach(() => { + resetLogger(); + setLogLevel(LogLevel.DEBUG); + }); + + afterEach(() => { + resetLogger(); + }); + + describe('custom logger', () => { + it('routes log messages to custom logger', () => { + const messages: Array<{level: string; args: unknown[]}> = []; + const customLogger: Logger = { + log: (level, ...args) => messages.push({level: LogLevel[level], args}), + debug: (...args) => messages.push({level: 'DEBUG', args}), + info: (...args) => messages.push({level: 'INFO', args}), + warn: (...args) => messages.push({level: 'WARN', args}), + error: (...args) => messages.push({level: 'ERROR', args}), + }; + + setLogger(customLogger); + const logger = getLogger(); + + logger.info('test message', 123); + + expect(messages).toHaveLength(1); + expect(messages[0].level).toBe('INFO'); + expect(messages[0].args).toEqual(['test message', 123]); + }); + + it('calls correct method for each log level', () => { + const calls: string[] = []; + const customLogger: Logger = { + log: () => calls.push('log'), + debug: () => calls.push('debug'), + info: () => calls.push('info'), + warn: () => calls.push('warn'), + error: () => calls.push('error'), + }; + + setLogger(customLogger); + const logger = getLogger(); + + logger.debug('debug'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + + expect(calls).toEqual(['debug', 'info', 'warn', 'error']); + }); + }); + + describe('null logger (disable logging)', () => { + it('disables all logging when null is passed', () => { + const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); + + setLogger(null); + const logger = getLogger(); + + logger.info('this should not appear'); + + expect(consoleSpy).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it('handles all log levels silently', () => { + setLogger(null); + const logger = getLogger(); + + expect(() => { + logger.debug('debug'); + logger.info('info'); + logger.warn('warn'); + logger.error('error'); + logger.log(LogLevel.INFO, 'log'); + }).not.toThrow(); + }); + }); + + describe('backward compatibility', () => { + it('deprecated logger export still works with custom logger', async () => { + const {logger} = await import('../../src/utils/logger.js'); + + const messages: string[] = []; + const customLogger: Logger = { + log: () => {}, + debug: () => {}, + info: (...args) => messages.push(String(args[0])), + warn: () => {}, + error: () => {}, + }; + + setLogger(customLogger); + + logger.info('backward compatible'); + + expect(messages).toContain('backward compatible'); + }); + }); + + describe('getLogger', () => { + it('returns the current logger instance', () => { + const customLogger: Logger = { + log: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }; + + setLogger(customLogger); + + const logger = getLogger(); + expect(logger).toBeDefined(); + }); + + it('returns default logger initially', () => { + const logger = getLogger(); + expect(logger).toBeDefined(); + expect(typeof logger.info).toBe('function'); + }); + }); + + describe('resetLogger', () => { + it('restores the default logger', () => { + const consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); + + setLogger(null); + resetLogger(); + + const logger = getLogger(); + logger.info('after reset'); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); +});