-
Notifications
You must be signed in to change notification settings - Fork 14.4k
feat(core): expose RAG snippets to local log file for debugging #27016
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
c434478
feat(core): expose RAG snippets to local log file for debugging
spencer426 7e333b5
fix(core): secure rag logger file permissions and types
spencer426 fd10534
Merge branch 'main' into expose-rag
spencer426 15e4fa7
fix(core): improve rag logger atomicity and testing
spencer426 d899c90
fix(core): remove eslint-disable and safely extract metadata
spencer426 c2763f9
Merge branch 'main' into expose-rag
spencer426 2117cc8
test(core): fix fake timers usage in ragLogger tests
spencer426 7ac5f14
Merge branch 'main' into expose-rag
spencer426 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||
| import * as fs from 'node:fs'; | ||
| import * as path from 'node:path'; | ||
| import { RagLogger } from './ragLogger.js'; | ||
| import { debugLogger } from './debugLogger.js'; | ||
|
|
||
| vi.mock('node:fs', () => ({ | ||
| existsSync: vi.fn(), | ||
| mkdirSync: vi.fn(), | ||
| openSync: vi.fn(), | ||
| fchmodSync: vi.fn(), | ||
| writeSync: vi.fn(), | ||
| closeSync: vi.fn(), | ||
| chmodSync: vi.fn(), | ||
| realpathSync: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock('./debugLogger.js', () => ({ | ||
| debugLogger: { | ||
| error: vi.fn(), | ||
| warn: vi.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| describe('RagLogger', () => { | ||
| let logger: RagLogger; | ||
|
|
||
| beforeEach(() => { | ||
| logger = new RagLogger(); | ||
| vi.clearAllMocks(); | ||
| vi.useFakeTimers({ now: new Date('2026-05-13T12:00:00.000Z') }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| describe('initialize', () => { | ||
| it('should create the logs directory if it does not exist', () => { | ||
| vi.mocked(fs.realpathSync).mockReturnValue('/real/test/logs'); | ||
|
|
||
| logger.initialize('/test/logs'); | ||
|
|
||
| expect(fs.mkdirSync).toHaveBeenCalledWith('/test/logs', { | ||
| recursive: true, | ||
| mode: 0o700, | ||
| }); | ||
| expect(fs.realpathSync).toHaveBeenCalledWith('/test/logs'); | ||
| expect(fs.chmodSync).toHaveBeenCalledWith('/real/test/logs', 0o700); | ||
| }); | ||
|
|
||
| it('should log an error to debugLogger if directory creation fails', () => { | ||
| const error = new Error('mkdir failed'); | ||
| vi.mocked(fs.mkdirSync).mockImplementation(() => { | ||
| throw error; | ||
| }); | ||
|
|
||
| logger.initialize('/test/logs'); | ||
|
|
||
| expect(debugLogger.error).toHaveBeenCalledWith( | ||
| 'Failed to create or set permissions for rag-trace.log directory', | ||
| error, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('log', () => { | ||
| it('should warn if called before initialization', () => { | ||
| logger.log({ sessionId: '123', ragStatus: 'SUCCESS', snippets: [] }); | ||
|
|
||
| expect(debugLogger.warn).toHaveBeenCalledWith( | ||
| 'RagLogger was called before being initialized.', | ||
| ); | ||
| expect(fs.openSync).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should create log entry atomically and enforce permissions on first run', () => { | ||
| logger.initialize('/test/logs'); | ||
|
|
||
| const entry = { | ||
| sessionId: 'session-1', | ||
| ragStatus: 'SUCCESS', | ||
| snippets: [{ content: 'test snippet', relevanceScore: 0.9 }], | ||
| }; | ||
|
|
||
| vi.mocked(fs.openSync).mockReturnValue(42); | ||
|
|
||
| logger.log(entry); | ||
|
|
||
| const expectedFullEntry = { | ||
| timestamp: '2026-05-13T12:00:00.000Z', | ||
| ...entry, | ||
| }; | ||
|
|
||
| expect(fs.openSync).toHaveBeenCalledWith( | ||
| path.join('/test/logs', 'rag-trace.log'), | ||
| 'a', | ||
| 0o600, | ||
| ); | ||
| expect(fs.fchmodSync).toHaveBeenCalledWith(42, 0o600); | ||
| expect(fs.writeSync).toHaveBeenCalledWith( | ||
| 42, | ||
| JSON.stringify(expectedFullEntry) + '\n', | ||
| null, | ||
| 'utf8', | ||
| ); | ||
| expect(fs.closeSync).toHaveBeenCalledWith(42); | ||
|
|
||
| // Subsequent logs should not call fchmodSync again | ||
| vi.mocked(fs.fchmodSync).mockClear(); | ||
| logger.log(entry); | ||
| expect(fs.fchmodSync).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should log an error to debugLogger if writing to file fails', () => { | ||
| logger.initialize('/test/logs'); | ||
|
|
||
| const error = new Error('open failed'); | ||
| vi.mocked(fs.openSync).mockImplementation(() => { | ||
| throw error; | ||
| }); | ||
|
|
||
| logger.log({ sessionId: '123', ragStatus: 'SUCCESS', snippets: [] }); | ||
|
|
||
| expect(debugLogger.error).toHaveBeenCalledWith( | ||
| `Failed to write to ${path.join('/test/logs', 'rag-trace.log')}`, | ||
| error, | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2026 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import * as fs from 'node:fs'; | ||
| import * as path from 'node:path'; | ||
| import { debugLogger } from './debugLogger.js'; | ||
|
|
||
| export interface RagSnippet { | ||
| repository?: string; | ||
| filePath?: string; | ||
| startLine?: number; | ||
| endLine?: number; | ||
| relevanceScore?: number; | ||
| content: string; | ||
| } | ||
|
|
||
| export interface RagLogEntry { | ||
| timestamp: string; | ||
| sessionId: string; | ||
| ragStatus: string; | ||
| snippets: RagSnippet[]; | ||
| } | ||
|
|
||
| export class RagLogger { | ||
| private logPath: string | undefined; | ||
| private hasInitializedFile = false; | ||
|
|
||
| /** | ||
| * Initializes the logger with the project's temporary logs directory. | ||
| */ | ||
| initialize(logsDir: string) { | ||
| this.logPath = path.join(logsDir, 'rag-trace.log'); | ||
|
|
||
| // Ensure the directory exists | ||
| try { | ||
| fs.mkdirSync(logsDir, { recursive: true, mode: 0o700 }); | ||
| const actualPath = fs.realpathSync(logsDir); | ||
| fs.chmodSync(actualPath, 0o700); | ||
| } catch (e) { | ||
| debugLogger.error( | ||
| 'Failed to create or set permissions for rag-trace.log directory', | ||
| e, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Logs a RAG trace entry as JSONL. | ||
| */ | ||
| log(entry: Omit<RagLogEntry, 'timestamp'>) { | ||
| if (!this.logPath) { | ||
| debugLogger.warn('RagLogger was called before being initialized.'); | ||
| return; | ||
| } | ||
|
|
||
| const fullEntry: RagLogEntry = { | ||
| timestamp: new Date().toISOString(), | ||
| ...entry, | ||
| }; | ||
|
|
||
| try { | ||
| // Use openSync to atomically create the file with strict permissions | ||
| const fd = fs.openSync(this.logPath, 'a', 0o600); | ||
|
|
||
| if (!this.hasInitializedFile) { | ||
| // Ensure permissions are strict even if the file was pre-created | ||
| fs.fchmodSync(fd, 0o600); | ||
| this.hasInitializedFile = true; | ||
| } | ||
|
|
||
| fs.writeSync(fd, JSON.stringify(fullEntry) + '\n', null, 'utf8'); | ||
| fs.closeSync(fd); | ||
| } catch (e) { | ||
| debugLogger.error(`Failed to write to ${this.logPath}`, e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const ragLogger = new RagLogger(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.