Skip to content

Commit e1bab57

Browse files
Dwij1704dot-agi
andauthored
Refactor logging to use in-memory buffer instead of file. (#961)
* Refactor logging to use in-memory buffer instead of file. * rename file_logger to buffer_logger * Enhance print logger setup to prevent duplicate handlers and ensure original print function is only replaced once. * Refactor instrument logging tests to use in-memory buffer and improve fixture management. --------- Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
1 parent 96e3059 commit e1bab57

2 files changed

Lines changed: 62 additions & 69 deletions

File tree

agentops/logging/instrument_logging.py

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,64 @@
33
import os
44
import atexit
55
from typing import Any
6+
from io import StringIO
67

78
_original_print = builtins.print
89

9-
LOGFILE_NAME = "agentops-tmp.log"
10-
11-
# Instrument loggers and print function to log to a file
12-
10+
# Global buffer to store logs
11+
_log_buffer = StringIO()
1312

1413
def setup_print_logger() -> None:
1514
"""
16-
~Monkeypatches~ *Instruments the built-in print function and configures logging to also log to a file.
15+
Instruments the built-in print function and configures logging to use a memory buffer.
1716
Preserves existing logging configuration and console output behavior.
1817
"""
19-
log_file = os.path.join(os.getcwd(), LOGFILE_NAME)
18+
buffer_logger = logging.getLogger('agentops_buffer_logger')
19+
buffer_logger.setLevel(logging.DEBUG)
2020

21-
file_logger = logging.getLogger('agentops_file_logger')
22-
file_logger.setLevel(logging.DEBUG)
21+
# Check if the logger already has handlers to prevent duplicates
22+
if not buffer_logger.handlers:
23+
# Create a StreamHandler that writes to our StringIO buffer
24+
buffer_handler = logging.StreamHandler(_log_buffer)
25+
buffer_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
26+
buffer_handler.setLevel(logging.DEBUG)
27+
buffer_logger.addHandler(buffer_handler)
2328

24-
file_handler = logging.FileHandler(log_file)
25-
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
26-
file_handler.setLevel(logging.DEBUG)
27-
file_logger.addHandler(file_handler)
28-
29-
# Ensure the new logger doesn't propagate to root
30-
file_logger.propagate = False
29+
# Ensure the new logger doesn't propagate to root
30+
buffer_logger.propagate = False
3131

3232
def print_logger(*args: Any, **kwargs: Any) -> None:
3333
"""
34-
Custom print function that logs to file and console.
34+
Custom print function that logs to buffer and console.
3535
3636
Args:
3737
*args: Arguments to print
3838
**kwargs: Keyword arguments to print
3939
"""
4040
message = " ".join(str(arg) for arg in args)
41-
file_logger.info(message)
41+
buffer_logger.info(message)
4242

4343
# print to console using original print
4444
_original_print(*args, **kwargs)
4545

46-
# replace the built-in print with ours
47-
builtins.print = print_logger
46+
# Only replace print if it hasn't been replaced already
47+
if builtins.print is _original_print:
48+
builtins.print = print_logger
4849

4950
def cleanup():
5051
"""
5152
Cleanup function to be called when the process exits.
52-
Removes the log file and restores the original print function.
53+
Restores the original print function and clears the buffer.
5354
"""
5455
try:
55-
# Remove our file handler
56-
for handler in file_logger.handlers[:]:
56+
# Remove our buffer handler
57+
for handler in buffer_logger.handlers[:]:
5758
handler.close()
58-
file_logger.removeHandler(handler)
59+
buffer_logger.removeHandler(handler)
60+
61+
# Clear the buffer
62+
_log_buffer.seek(0)
63+
_log_buffer.truncate()
5964

6065
# Restore the original print function
6166
builtins.print = _original_print
@@ -69,17 +74,18 @@ def cleanup():
6974

7075
def upload_logfile(trace_id: int) -> None:
7176
"""
72-
Upload the log file to the API.
77+
Upload the log content from the memory buffer to the API.
7378
"""
7479
from agentops import get_client
7580

76-
log_file = os.path.join(os.getcwd(), LOGFILE_NAME)
77-
if not os.path.exists(log_file):
81+
# Get the content from the buffer
82+
log_content = _log_buffer.getvalue()
83+
if not log_content:
7884
return
79-
with open(log_file, "r") as f:
80-
log_content = f.read()
8185

8286
client = get_client()
8387
client.api.v4.upload_logfile(log_content, trace_id)
8488

85-
os.remove(log_file)
89+
# Clear the buffer after upload
90+
_log_buffer.seek(0)
91+
_log_buffer.truncate()

tests/unit/logging/test_instrument_logging.py

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,56 @@
22
import builtins
33
import pytest
44
from unittest.mock import patch, MagicMock
5-
from agentops.logging.instrument_logging import setup_print_logger, upload_logfile, LOGFILE_NAME
5+
from agentops.logging.instrument_logging import setup_print_logger, upload_logfile
66
import logging
7-
87
@pytest.fixture
9-
def cleanup_log_file():
10-
"""Fixture to clean up the log file before and after tests"""
11-
log_file = os.path.join(os.getcwd(), LOGFILE_NAME)
12-
if os.path.exists(log_file):
13-
os.remove(log_file)
8+
def reset_print():
9+
"""Fixture to reset the print function after tests"""
10+
original_print = builtins.print
1411
yield
15-
if os.path.exists(log_file):
16-
os.remove(log_file)
12+
builtins.print = original_print
1713

18-
def test_setup_print_logger_creates_log_file(cleanup_log_file):
19-
"""Test that setup_print_logger creates a log file"""
14+
def test_setup_print_logger_creates_buffer_logger_and_handler():
15+
"""Test that setup_print_logger creates a buffer logger with a StreamHandler."""
2016
setup_print_logger()
21-
log_file = os.path.join(os.getcwd(), LOGFILE_NAME)
22-
assert os.path.exists(log_file)
17+
buffer_logger = logging.getLogger('agentops_buffer_logger')
18+
assert buffer_logger.level == logging.DEBUG
19+
assert len(buffer_logger.handlers) == 1
20+
assert isinstance(buffer_logger.handlers[0], logging.StreamHandler)
2321

24-
def test_print_logger_writes_to_file(cleanup_log_file):
25-
"""Test that the monkeypatched print function writes to the log file"""
22+
def test_print_logger_writes_message_to_stringio_buffer(reset_print):
23+
"""Test that the monkeypatched print function writes messages to the StringIO buffer."""
2624
setup_print_logger()
2725
test_message = "Test log message"
2826
print(test_message)
29-
30-
log_file = os.path.join(os.getcwd(), LOGFILE_NAME)
31-
with open(log_file, 'r') as f:
32-
log_content = f.read()
33-
assert test_message in log_content
27+
buffer_logger = logging.getLogger('agentops_buffer_logger')
28+
log_content = buffer_logger.handlers[0].stream.getvalue()
29+
assert test_message in log_content
3430

35-
def test_print_logger_preserves_original_print(cleanup_log_file):
36-
"""Test that the original print function is preserved"""
31+
def test_print_logger_replaces_and_restores_builtin_print(reset_print):
32+
"""Test that setup_print_logger replaces builtins.print and the fixture restores it after the test."""
33+
import agentops.logging.instrument_logging as il
34+
builtins.print = il._original_print
3735
original_print = builtins.print
3836
setup_print_logger()
3937
assert builtins.print != original_print
40-
41-
# Cleanup should restore original print
42-
for handler in logging.getLogger('agentops_file_logger').handlers[:]:
43-
handler.close()
44-
logging.getLogger('agentops_file_logger').removeHandler(handler)
45-
builtins.print = original_print
38+
# The reset_print fixture will restore print after the test
4639

4740
@patch('agentops.get_client')
48-
def test_upload_logfile(mock_get_client, cleanup_log_file):
49-
"""Test that upload_logfile reads and uploads log content"""
50-
# Setup
41+
def test_upload_logfile_sends_buffer_content_and_clears_buffer(mock_get_client):
42+
"""Test that upload_logfile uploads the buffer content and clears the buffer after upload."""
5143
setup_print_logger()
5244
test_message = "Test upload message"
5345
print(test_message)
54-
55-
# Mock the client
5646
mock_client = MagicMock()
5747
mock_get_client.return_value = mock_client
58-
59-
# Test upload
6048
upload_logfile(trace_id=123)
61-
62-
# Verify
6349
mock_client.api.v4.upload_logfile.assert_called_once()
64-
assert not os.path.exists(os.path.join(os.getcwd(), LOGFILE_NAME))
50+
buffer_logger = logging.getLogger('agentops_buffer_logger')
51+
assert buffer_logger.handlers[0].stream.getvalue() == ""
6552

66-
def test_upload_logfile_nonexistent_file():
67-
"""Test that upload_logfile handles nonexistent log file gracefully"""
53+
def test_upload_logfile_does_nothing_when_buffer_is_empty():
54+
"""Test that upload_logfile does nothing and does not call the client when the buffer is empty."""
6855
with patch('agentops.get_client') as mock_get_client:
6956
upload_logfile(trace_id=123)
7057
mock_get_client.assert_not_called()

0 commit comments

Comments
 (0)