diff --git a/core/src/common.ts b/core/src/common.ts index 23f628165..3b33d3c8a 100644 --- a/core/src/common.ts +++ b/core/src/common.ts @@ -280,7 +280,13 @@ export type { } from './tools/vertex_ai_search_tool.js'; export {VertexRagRetrievalTool} from './tools/vertex_rag_retrieval_tool.js'; export {getClientLabels, runWithClientLabel} from './utils/client_labels.js'; -export {LogLevel, getLogger, setLogLevel, setLogger} from './utils/logger.js'; +export { + LogLevel, + getLogLevel, + getLogger, + setLogLevel, + setLogger, +} from './utils/logger.js'; export type {Logger} from './utils/logger.js'; export {isGemini2OrAbove, isGemini3xFlashLive} from './utils/model_name.js'; export {zodObjectToSchema} from './utils/simple_zod_to_json.js'; diff --git a/core/src/utils/logger.ts b/core/src/utils/logger.ts index dc854e7ec..dbe0ee250 100644 --- a/core/src/utils/logger.ts +++ b/core/src/utils/logger.ts @@ -28,6 +28,15 @@ export interface Logger { error(...args: unknown[]): void; setLogLevel(level: LogLevel): void; + + /** + * Returns the minimum level this logger emits, or `undefined` if it has no + * single level to report. + * + * Optional so that existing `Logger` implementations keep compiling. Prefer + * the module-level `getLogLevel()` over calling this directly. + */ + getLogLevel?(): LogLevel | undefined; } class SimpleLogger implements Logger { @@ -63,6 +72,10 @@ class SimpleLogger implements Logger { this.logLevel = level; } + getLogLevel(): LogLevel { + return this.logLevel; + } + log(level: LogLevel, ...messages: unknown[]): void { if (this.logLevel > level) { return; @@ -146,6 +159,19 @@ export function setLogLevel(level: LogLevel) { logger.setLogLevel(level); } +/** + * Gets the current log level, or `undefined` if the active logger does not + * report one (for example a custom logger that predates `getLogLevel`, or the + * no-op logger installed by `setLogger(null)`). + * + * Callers using this as an "is this level enabled?" guard should treat + * `undefined` as enabled, so that a custom logger still receives messages it + * may want to emit. + */ +export function getLogLevel(): LogLevel | undefined { + return logger.getLogLevel?.(); +} + /** * The logger instance for ADK. */ @@ -153,6 +179,9 @@ export const logger: Logger = { setLogLevel(level: LogLevel): void { currentLogger.setLogLevel(level); }, + getLogLevel(): LogLevel | undefined { + return currentLogger.getLogLevel?.(); + }, log(level: LogLevel, ...args: unknown[]): void { currentLogger.log(level, ...args); }, diff --git a/core/test/utils/logger_test.ts b/core/test/utils/logger_test.ts index 2865a9de0..5cfd3f364 100644 --- a/core/test/utils/logger_test.ts +++ b/core/test/utils/logger_test.ts @@ -4,9 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {getLogger, Logger, LogLevel, setLogger, setLogLevel} from '@google/adk'; +import { + getLogger, + getLogLevel, + Logger, + LogLevel, + setLogger, + setLogLevel, +} from '@google/adk'; import {afterEach, beforeEach, describe, expect, it} from 'vitest'; -import {resetLogger} from '../../src/utils/logger.js'; +import {logger as loggerFacade, resetLogger} from '../../src/utils/logger.js'; describe('setLogger', () => { beforeEach(() => { @@ -142,3 +149,122 @@ describe('setLogger', () => { }); }); }); + +const LEVEL_CASES = [ + LogLevel.DEBUG, + LogLevel.INFO, + LogLevel.WARN, + LogLevel.ERROR, +].map((level) => ({name: LogLevel[level], level})); + +describe('getLogLevel', () => { + beforeEach(() => { + resetLogger(); + }); + + afterEach(() => { + resetLogger(); + }); + + describe('default logger', () => { + it('returns INFO for the default logger', () => { + expect(getLogLevel()).toBe(LogLevel.INFO); + }); + + it.each(LEVEL_CASES)('reflects setLogLevel($name)', ({level}) => { + setLogLevel(level); + + expect(getLogLevel()).toBe(level); + }); + }); + + describe('custom logger that implements getLogLevel', () => { + it('returns the level reported by the custom logger', () => { + const customLogger: Logger = { + setLogLevel: () => {}, + log: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + getLogLevel: () => LogLevel.WARN, + }; + + setLogger(customLogger); + + expect(getLogLevel()).toBe(LogLevel.WARN); + }); + + it('delegates on every call instead of caching a copy', () => { + let level = LogLevel.INFO; + const customLogger: Logger = { + setLogLevel: (next) => { + level = next; + }, + log: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + getLogLevel: () => level, + }; + + setLogger(customLogger); + setLogLevel(LogLevel.ERROR); + + expect(getLogLevel()).toBe(LogLevel.ERROR); + }); + }); + + describe('custom logger that does not implement getLogLevel', () => { + const customLogger: Logger = { + setLogLevel: () => {}, + log: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }; + + it('returns undefined', () => { + setLogger(customLogger); + + expect(getLogLevel()).toBeUndefined(); + }); + + it('does not throw', () => { + setLogger(customLogger); + + expect(() => getLogLevel()).not.toThrow(); + }); + }); + + describe('null logger', () => { + it('returns undefined for the no-op logger', () => { + setLogger(null); + + expect(getLogLevel()).toBeUndefined(); + }); + + it('returns INFO again after resetLogger', () => { + setLogger(null); + resetLogger(); + + expect(getLogLevel()).toBe(LogLevel.INFO); + }); + }); + + describe('logger facade', () => { + it('forwards to the active logger', () => { + setLogLevel(LogLevel.WARN); + + expect(loggerFacade.getLogLevel?.()).toBe(LogLevel.WARN); + }); + + it('reports undefined when the active logger has no level', () => { + setLogger(null); + + expect(loggerFacade.getLogLevel?.()).toBeUndefined(); + }); + }); +});