From 0573c0a49fe4b532baf9d0e8ff01124843b45585 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Thu, 17 Apr 2025 18:24:48 -0700 Subject: [PATCH 1/7] capture logger to file --- agentops/instrumentation/common/logs.py | 80 +++++++++++++++++++++++++ agentops/sdk/core.py | 14 ++++- agentops/sdk/types.py | 1 + 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 agentops/instrumentation/common/logs.py diff --git a/agentops/instrumentation/common/logs.py b/agentops/instrumentation/common/logs.py new file mode 100644 index 000000000..f8550fe1f --- /dev/null +++ b/agentops/instrumentation/common/logs.py @@ -0,0 +1,80 @@ +import builtins +import logging +import os +import atexit +from datetime import datetime +from typing import Any, TextIO + +# Store the original print function +_original_print = builtins.print + +def setup_print_logger() -> None: + """ + Monkeypatches the built-in print function and configures logging to also log to a file. + Preserves existing logging configuration and console output behavior. + """ + # Create a unique log file name with timestamp + log_file = os.path.join(os.getcwd(), f"agentops-tmp.log") + + # Get the root logger + root_logger = logging.getLogger() + + # Add our file handler without removing existing ones + file_handler = logging.FileHandler(log_file) + file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + file_handler.setLevel(logging.INFO) # Capture all log levels + root_logger.addHandler(file_handler) + + # Set root logger level to DEBUG to ensure we capture everything + root_logger.setLevel(logging.DEBUG) + + # Test logging + logging.debug("Test debug message") + logging.info("Test info message") + logging.warning("Test warning message") + + def print_logger(*args: Any, **kwargs: Any) -> None: + """ + Custom print function that logs to file and console. + + Args: + *args: Arguments to print + **kwargs: Keyword arguments to print + """ + # Convert all arguments to strings and join them + message = " ".join(str(arg) for arg in args) + + # First log to file + logging.info(message) + + # Then print to console using original print + _original_print(*args, **kwargs) + + # Replace the built-in print with our custom version + builtins.print = print_logger + + def cleanup(): + """ + Cleanup function to be called when the process exits. + Removes the log file and restores the original print function. + """ + try: + # Only remove our file handler + for handler in root_logger.handlers[:]: + if isinstance(handler, logging.FileHandler) and handler.baseFilename == log_file: + handler.close() + root_logger.removeHandler(handler) + + # Delete the log file + if os.path.exists(log_file): + # os.remove(log_file) + pass + + # Restore the original print function + builtins.print = _original_print + except Exception as e: + # If something goes wrong during cleanup, just print the error + _original_print(f"Error during cleanup: {e}") + + # Register the cleanup function to run when the process exits + atexit.register(cleanup) \ No newline at end of file diff --git a/agentops/sdk/core.py b/agentops/sdk/core.py index 540089e8c..842f4ba2e 100644 --- a/agentops/sdk/core.py +++ b/agentops/sdk/core.py @@ -1,6 +1,7 @@ from __future__ import annotations import atexit +import logging import threading from typing import List, Optional @@ -16,6 +17,7 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor from agentops.exceptions import AgentOpsClientNotInitializedException +from agentops.instrumentation.common.logs import setup_print_logger from agentops.logging import logger from agentops.sdk.processors import InternalSpanProcessor from agentops.sdk.types import TracingConfig @@ -29,6 +31,7 @@ def setup_telemetry( project_id: Optional[str] = None, exporter_endpoint: str = "https://otlp.agentops.ai/v1/traces", metrics_endpoint: str = "https://otlp.agentops.ai/v1/metrics", + logs_endpoint: str = "https://otlp.agentops.ai/v1/logs", max_queue_size: int = 512, max_wait_time: int = 5000, export_flush_interval: int = 1000, @@ -42,6 +45,7 @@ def setup_telemetry( project_id: Project ID to include in resource attributes exporter_endpoint: Endpoint for the span exporter metrics_endpoint: Endpoint for the metrics exporter + logs_endpoint: Endpoint for the logs exporter max_queue_size: Maximum number of spans to queue before forcing a flush max_wait_time: Maximum time in milliseconds to wait before flushing export_flush_interval: Time interval in milliseconds between automatic exports of telemetry data @@ -90,6 +94,9 @@ def setup_telemetry( meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader]) metrics.set_meter_provider(meter_provider) + ### Logging + setup_print_logger() + logger.debug("Telemetry system initialized") return provider, meter_provider @@ -151,6 +158,7 @@ def initialize(self, jwt: Optional[str] = None, **kwargs) -> None: kwargs.setdefault("service_name", "agentops") kwargs.setdefault("exporter_endpoint", "https://otlp.agentops.ai/v1/traces") kwargs.setdefault("metrics_endpoint", "https://otlp.agentops.ai/v1/metrics") + kwargs.setdefault("logs_endpoint", "https://otlp.agentops.ai/v1/logs") kwargs.setdefault("max_queue_size", 512) kwargs.setdefault("max_wait_time", 5000) kwargs.setdefault("export_flush_interval", 1000) @@ -160,6 +168,7 @@ def initialize(self, jwt: Optional[str] = None, **kwargs) -> None: "service_name": kwargs["service_name"], "exporter_endpoint": kwargs["exporter_endpoint"], "metrics_endpoint": kwargs["metrics_endpoint"], + "logs_endpoint": kwargs["logs_endpoint"], "max_queue_size": kwargs["max_queue_size"], "max_wait_time": kwargs["max_wait_time"], "export_flush_interval": kwargs["export_flush_interval"], @@ -173,8 +182,9 @@ def initialize(self, jwt: Optional[str] = None, **kwargs) -> None: self._provider, self._meter_provider = setup_telemetry( service_name=config["service_name"] or "", project_id=config.get("project_id"), - exporter_endpoint=config["exporter_endpoint"] or "", - metrics_endpoint=config["metrics_endpoint"] or "", + exporter_endpoint=config["exporter_endpoint"], + metrics_endpoint=config["metrics_endpoint"], + logs_endpoint=config["logs_endpoint"], max_queue_size=config["max_queue_size"], max_wait_time=config["max_wait_time"], export_flush_interval=config["export_flush_interval"], diff --git a/agentops/sdk/types.py b/agentops/sdk/types.py index b8af98d1e..10a2af8dc 100644 --- a/agentops/sdk/types.py +++ b/agentops/sdk/types.py @@ -14,6 +14,7 @@ class TracingConfig(TypedDict, total=False): processor: Optional[SpanProcessor] exporter_endpoint: Optional[str] metrics_endpoint: Optional[str] + logs_endpoint: Optional[str] api_key: Optional[str] # API key for authentication with AgentOps services project_id: Optional[str] # Project ID to include in resource attributes max_queue_size: int # Required with a default value From 6d3ea35c84842ec451ab42f8c0dba9eb55498ce2 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Thu, 17 Apr 2025 18:45:17 -0700 Subject: [PATCH 2/7] dont interfere with other loggers --- agentops/instrumentation/common/logs.py | 32 ++++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/agentops/instrumentation/common/logs.py b/agentops/instrumentation/common/logs.py index f8550fe1f..00f9bcc18 100644 --- a/agentops/instrumentation/common/logs.py +++ b/agentops/instrumentation/common/logs.py @@ -19,19 +19,18 @@ def setup_print_logger() -> None: # Get the root logger root_logger = logging.getLogger() - # Add our file handler without removing existing ones + # Create a new logger specifically for our file logging + file_logger = logging.getLogger('agentops_file_logger') + file_logger.setLevel(logging.DEBUG) + + # Add our file handler to the new logger file_handler = logging.FileHandler(log_file) file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) - file_handler.setLevel(logging.INFO) # Capture all log levels - root_logger.addHandler(file_handler) - - # Set root logger level to DEBUG to ensure we capture everything - root_logger.setLevel(logging.DEBUG) + file_handler.setLevel(logging.DEBUG) + file_logger.addHandler(file_handler) - # Test logging - logging.debug("Test debug message") - logging.info("Test info message") - logging.warning("Test warning message") + # Ensure the new logger doesn't propagate to root + file_logger.propagate = False def print_logger(*args: Any, **kwargs: Any) -> None: """ @@ -44,8 +43,8 @@ def print_logger(*args: Any, **kwargs: Any) -> None: # Convert all arguments to strings and join them message = " ".join(str(arg) for arg in args) - # First log to file - logging.info(message) + # Log to our file logger + file_logger.info(message) # Then print to console using original print _original_print(*args, **kwargs) @@ -59,11 +58,10 @@ def cleanup(): Removes the log file and restores the original print function. """ try: - # Only remove our file handler - for handler in root_logger.handlers[:]: - if isinstance(handler, logging.FileHandler) and handler.baseFilename == log_file: - handler.close() - root_logger.removeHandler(handler) + # Remove our file handler + for handler in file_logger.handlers[:]: + handler.close() + file_logger.removeHandler(handler) # Delete the log file if os.path.exists(log_file): From cc8cf906013ccb1cb7794fa2d72382a9a0ba4457 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Fri, 18 Apr 2025 16:54:06 -0700 Subject: [PATCH 3/7] send logs --- agentops/client/api/versions/v4.py | 31 +++++++++++++++++++++++++ agentops/instrumentation/common/logs.py | 30 +++++++++++++++++++++--- agentops/sdk/core.py | 5 ---- agentops/sdk/processors.py | 3 ++- 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/agentops/client/api/versions/v4.py b/agentops/client/api/versions/v4.py index 295a1c650..68f035041 100644 --- a/agentops/client/api/versions/v4.py +++ b/agentops/client/api/versions/v4.py @@ -68,4 +68,35 @@ def upload_object(self, body: Union[str, bytes]) -> UploadedObjectResponse: return UploadedObjectResponse(**response_data) except Exception as e: raise ApiServerException(f"Failed to process upload response: {str(e)}") + + + def upload_logfile(self, body: Union[str, bytes], trace_id: int) -> UploadedObjectResponse: + """ + Upload an log file to the API and return the response. + + Args: + body: The log file to upload, either as a string or bytes. + Returns: + UploadedObjectResponse: The response from the API after upload. + """ + if isinstance(body, bytes): + body = body.decode("utf-8") + + response = self.post("/v4/logs/upload/", body, {**self.prepare_headers(), "Trace-Id": str(trace_id)}) + + if response.status_code != 200: + error_msg = f"Upload failed: {response.status_code}" + try: + error_data = response.json() + if "error" in error_data: + error_msg = error_data["error"] + except Exception: + pass + raise ApiServerException(error_msg) + + try: + response_data = response.json() + return UploadedObjectResponse(**response_data) + except Exception as e: + raise ApiServerException(f"Failed to process upload response: {str(e)}") diff --git a/agentops/instrumentation/common/logs.py b/agentops/instrumentation/common/logs.py index 00f9bcc18..c1106b02e 100644 --- a/agentops/instrumentation/common/logs.py +++ b/agentops/instrumentation/common/logs.py @@ -4,17 +4,21 @@ import atexit from datetime import datetime from typing import Any, TextIO +from agents import Span # Store the original print function _original_print = builtins.print +LOGFILE_NAME = "agentops-tmp.log" + +## Instrument loggers and print function to log to a file def setup_print_logger() -> None: """ - Monkeypatches the built-in print function and configures logging to also log to a file. + ~Monkeypatches~ *Instruments the built-in print function and configures logging to also log to a file. Preserves existing logging configuration and console output behavior. """ # Create a unique log file name with timestamp - log_file = os.path.join(os.getcwd(), f"agentops-tmp.log") + log_file = os.path.join(os.getcwd(), LOGFILE_NAME) # Get the root logger root_logger = logging.getLogger() @@ -75,4 +79,24 @@ def cleanup(): _original_print(f"Error during cleanup: {e}") # Register the cleanup function to run when the process exits - atexit.register(cleanup) \ No newline at end of file + atexit.register(cleanup) + + +def upload_logfile(trace_id: int) -> None: + """ + Upload the log file to the API. + """ + from agentops import get_client + + log_file = os.path.join(os.getcwd(), LOGFILE_NAME) + # Check if the log file exists before attempting to upload + if not os.path.exists(log_file): + return + with open(log_file, "r") as f: + log_content = f.read() + + client = get_client() + client.api.v4.upload_logfile(log_content, trace_id) + + os.remove(log_file) + \ No newline at end of file diff --git a/agentops/sdk/core.py b/agentops/sdk/core.py index 842f4ba2e..9275b8068 100644 --- a/agentops/sdk/core.py +++ b/agentops/sdk/core.py @@ -31,7 +31,6 @@ def setup_telemetry( project_id: Optional[str] = None, exporter_endpoint: str = "https://otlp.agentops.ai/v1/traces", metrics_endpoint: str = "https://otlp.agentops.ai/v1/metrics", - logs_endpoint: str = "https://otlp.agentops.ai/v1/logs", max_queue_size: int = 512, max_wait_time: int = 5000, export_flush_interval: int = 1000, @@ -45,7 +44,6 @@ def setup_telemetry( project_id: Project ID to include in resource attributes exporter_endpoint: Endpoint for the span exporter metrics_endpoint: Endpoint for the metrics exporter - logs_endpoint: Endpoint for the logs exporter max_queue_size: Maximum number of spans to queue before forcing a flush max_wait_time: Maximum time in milliseconds to wait before flushing export_flush_interval: Time interval in milliseconds between automatic exports of telemetry data @@ -158,7 +156,6 @@ def initialize(self, jwt: Optional[str] = None, **kwargs) -> None: kwargs.setdefault("service_name", "agentops") kwargs.setdefault("exporter_endpoint", "https://otlp.agentops.ai/v1/traces") kwargs.setdefault("metrics_endpoint", "https://otlp.agentops.ai/v1/metrics") - kwargs.setdefault("logs_endpoint", "https://otlp.agentops.ai/v1/logs") kwargs.setdefault("max_queue_size", 512) kwargs.setdefault("max_wait_time", 5000) kwargs.setdefault("export_flush_interval", 1000) @@ -168,7 +165,6 @@ def initialize(self, jwt: Optional[str] = None, **kwargs) -> None: "service_name": kwargs["service_name"], "exporter_endpoint": kwargs["exporter_endpoint"], "metrics_endpoint": kwargs["metrics_endpoint"], - "logs_endpoint": kwargs["logs_endpoint"], "max_queue_size": kwargs["max_queue_size"], "max_wait_time": kwargs["max_wait_time"], "export_flush_interval": kwargs["export_flush_interval"], @@ -184,7 +180,6 @@ def initialize(self, jwt: Optional[str] = None, **kwargs) -> None: project_id=config.get("project_id"), exporter_endpoint=config["exporter_endpoint"], metrics_endpoint=config["metrics_endpoint"], - logs_endpoint=config["logs_endpoint"], max_queue_size=config["max_queue_size"], max_wait_time=config["max_wait_time"], export_flush_interval=config["export_flush_interval"], diff --git a/agentops/sdk/processors.py b/agentops/sdk/processors.py index 985907635..709f5aa16 100644 --- a/agentops/sdk/processors.py +++ b/agentops/sdk/processors.py @@ -16,7 +16,7 @@ from agentops.logging import logger from agentops.helpers.dashboard import log_trace_url from agentops.semconv.core import CoreAttributes - +from agentops.instrumentation.common.logs import upload_logfile class LiveSpanProcessor(SpanProcessor): def __init__(self, span_exporter: SpanExporter, **kwargs): @@ -127,6 +127,7 @@ def on_end(self, span: ReadableSpan) -> None: def shutdown(self) -> None: """Shutdown the processor.""" + upload_logfile(self._root_span_id) self._root_span_id = None def force_flush(self, timeout_millis: int = 30000) -> bool: From 1a6417948cbfe5f35f9973e2ebf835960fd209f2 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Fri, 18 Apr 2025 16:58:07 -0700 Subject: [PATCH 4/7] code cleanup --- agentops/instrumentation/common/logs.py | 20 ++------------------ agentops/sdk/core.py | 1 - agentops/sdk/types.py | 1 - 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/agentops/instrumentation/common/logs.py b/agentops/instrumentation/common/logs.py index c1106b02e..278bc4030 100644 --- a/agentops/instrumentation/common/logs.py +++ b/agentops/instrumentation/common/logs.py @@ -6,7 +6,6 @@ from typing import Any, TextIO from agents import Span -# Store the original print function _original_print = builtins.print LOGFILE_NAME = "agentops-tmp.log" @@ -17,17 +16,11 @@ def setup_print_logger() -> None: ~Monkeypatches~ *Instruments the built-in print function and configures logging to also log to a file. Preserves existing logging configuration and console output behavior. """ - # Create a unique log file name with timestamp log_file = os.path.join(os.getcwd(), LOGFILE_NAME) - # Get the root logger - root_logger = logging.getLogger() - - # Create a new logger specifically for our file logging file_logger = logging.getLogger('agentops_file_logger') file_logger.setLevel(logging.DEBUG) - # Add our file handler to the new logger file_handler = logging.FileHandler(log_file) file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) file_handler.setLevel(logging.DEBUG) @@ -44,16 +37,13 @@ def print_logger(*args: Any, **kwargs: Any) -> None: *args: Arguments to print **kwargs: Keyword arguments to print """ - # Convert all arguments to strings and join them message = " ".join(str(arg) for arg in args) - - # Log to our file logger file_logger.info(message) - # Then print to console using original print + # print to console using original print _original_print(*args, **kwargs) - # Replace the built-in print with our custom version + # replace the built-in print with ours builtins.print = print_logger def cleanup(): @@ -67,11 +57,6 @@ def cleanup(): handler.close() file_logger.removeHandler(handler) - # Delete the log file - if os.path.exists(log_file): - # os.remove(log_file) - pass - # Restore the original print function builtins.print = _original_print except Exception as e: @@ -89,7 +74,6 @@ def upload_logfile(trace_id: int) -> None: from agentops import get_client log_file = os.path.join(os.getcwd(), LOGFILE_NAME) - # Check if the log file exists before attempting to upload if not os.path.exists(log_file): return with open(log_file, "r") as f: diff --git a/agentops/sdk/core.py b/agentops/sdk/core.py index 9275b8068..60e259ae6 100644 --- a/agentops/sdk/core.py +++ b/agentops/sdk/core.py @@ -1,7 +1,6 @@ from __future__ import annotations import atexit -import logging import threading from typing import List, Optional diff --git a/agentops/sdk/types.py b/agentops/sdk/types.py index 10a2af8dc..b8af98d1e 100644 --- a/agentops/sdk/types.py +++ b/agentops/sdk/types.py @@ -14,7 +14,6 @@ class TracingConfig(TypedDict, total=False): processor: Optional[SpanProcessor] exporter_endpoint: Optional[str] metrics_endpoint: Optional[str] - logs_endpoint: Optional[str] api_key: Optional[str] # API key for authentication with AgentOps services project_id: Optional[str] # Project ID to include in resource attributes max_queue_size: int # Required with a default value From 7b13685ae864690e39bd5e7133b1a35b25787c93 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Mon, 21 Apr 2025 14:17:19 -0700 Subject: [PATCH 5/7] moved logging instrumentation --- agentops/logging/__init__.py | 3 ++- .../common/logs.py => logging/instrument_logging.py} | 0 agentops/sdk/core.py | 3 +-- agentops/sdk/processors.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename agentops/{instrumentation/common/logs.py => logging/instrument_logging.py} (100%) diff --git a/agentops/logging/__init__.py b/agentops/logging/__init__.py index 43fd391e4..167c4a673 100644 --- a/agentops/logging/__init__.py +++ b/agentops/logging/__init__.py @@ -1,3 +1,4 @@ from .config import configure_logging, logger +from .instrument_logging import setup_print_logger, upload_logfile -__all__ = ["logger", "configure_logging"] +__all__ = ["logger", "configure_logging", "setup_print_logger", "upload_logfile"] diff --git a/agentops/instrumentation/common/logs.py b/agentops/logging/instrument_logging.py similarity index 100% rename from agentops/instrumentation/common/logs.py rename to agentops/logging/instrument_logging.py diff --git a/agentops/sdk/core.py b/agentops/sdk/core.py index ec73ea96a..78b8129e2 100644 --- a/agentops/sdk/core.py +++ b/agentops/sdk/core.py @@ -21,8 +21,7 @@ from opentelemetry import context as context_api from agentops.exceptions import AgentOpsClientNotInitializedException -from agentops.instrumentation.common.logs import setup_print_logger -from agentops.logging import logger +from agentops.logging import logger, setup_print_logger from agentops.sdk.processors import InternalSpanProcessor from agentops.sdk.types import TracingConfig from agentops.semconv import ResourceAttributes diff --git a/agentops/sdk/processors.py b/agentops/sdk/processors.py index 709f5aa16..d2ebea99c 100644 --- a/agentops/sdk/processors.py +++ b/agentops/sdk/processors.py @@ -16,7 +16,7 @@ from agentops.logging import logger from agentops.helpers.dashboard import log_trace_url from agentops.semconv.core import CoreAttributes -from agentops.instrumentation.common.logs import upload_logfile +from agentops.logging import upload_logfile class LiveSpanProcessor(SpanProcessor): def __init__(self, span_exporter: SpanExporter, **kwargs): From a19e722a19dad676797019592f6df4eae3f5ea1a Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Wed, 23 Apr 2025 11:37:52 -0700 Subject: [PATCH 6/7] some testing --- agentops/sdk/processors.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/agentops/sdk/processors.py b/agentops/sdk/processors.py index d2ebea99c..c10fff868 100644 --- a/agentops/sdk/processors.py +++ b/agentops/sdk/processors.py @@ -12,7 +12,6 @@ from opentelemetry.sdk.trace import ReadableSpan, Span, SpanProcessor from opentelemetry.sdk.trace.export import SpanExporter -import agentops.semconv as semconv from agentops.logging import logger from agentops.helpers.dashboard import log_trace_url from agentops.semconv.core import CoreAttributes @@ -91,7 +90,7 @@ class InternalSpanProcessor(SpanProcessor): - This processor tries to use the native kind first, then falls back to the attribute """ - _root_span_id: Optional[Span] = None + _root_span_id: Optional[int] = None def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None: """ @@ -124,10 +123,13 @@ def on_end(self, span: ReadableSpan) -> None: if self._root_span_id and (span.context.span_id is self._root_span_id): logger.debug(f"[agentops.InternalSpanProcessor] Ending root span: {span.name}") log_trace_url(span) + try: + upload_logfile(span.context.trace_id) + except Exception as e: + logger.error(f"[agentops.InternalSpanProcessor] Error uploading logfile: {e}") def shutdown(self) -> None: """Shutdown the processor.""" - upload_logfile(self._root_span_id) self._root_span_id = None def force_flush(self, timeout_millis: int = 30000) -> bool: From 45530749a69fd016ae5bb9ae67ebb5f1412af797 Mon Sep 17 00:00:00 2001 From: Braelyn Boynton Date: Wed, 23 Apr 2025 12:00:44 -0700 Subject: [PATCH 7/7] some testing --- tests/unit/logging/test_instrument_logging.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/unit/logging/test_instrument_logging.py diff --git a/tests/unit/logging/test_instrument_logging.py b/tests/unit/logging/test_instrument_logging.py new file mode 100644 index 000000000..294f022de --- /dev/null +++ b/tests/unit/logging/test_instrument_logging.py @@ -0,0 +1,70 @@ +import os +import builtins +import pytest +from unittest.mock import patch, MagicMock +from agentops.logging.instrument_logging import setup_print_logger, upload_logfile, LOGFILE_NAME +import logging + +@pytest.fixture +def cleanup_log_file(): + """Fixture to clean up the log file before and after tests""" + log_file = os.path.join(os.getcwd(), LOGFILE_NAME) + if os.path.exists(log_file): + os.remove(log_file) + yield + if os.path.exists(log_file): + os.remove(log_file) + +def test_setup_print_logger_creates_log_file(cleanup_log_file): + """Test that setup_print_logger creates a log file""" + setup_print_logger() + log_file = os.path.join(os.getcwd(), LOGFILE_NAME) + assert os.path.exists(log_file) + +def test_print_logger_writes_to_file(cleanup_log_file): + """Test that the monkeypatched print function writes to the log file""" + setup_print_logger() + test_message = "Test log message" + print(test_message) + + log_file = os.path.join(os.getcwd(), LOGFILE_NAME) + with open(log_file, 'r') as f: + log_content = f.read() + assert test_message in log_content + +def test_print_logger_preserves_original_print(cleanup_log_file): + """Test that the original print function is preserved""" + original_print = builtins.print + setup_print_logger() + assert builtins.print != original_print + + # Cleanup should restore original print + for handler in logging.getLogger('agentops_file_logger').handlers[:]: + handler.close() + logging.getLogger('agentops_file_logger').removeHandler(handler) + builtins.print = original_print + +@patch('agentops.get_client') +def test_upload_logfile(mock_get_client, cleanup_log_file): + """Test that upload_logfile reads and uploads log content""" + # Setup + setup_print_logger() + test_message = "Test upload message" + print(test_message) + + # Mock the client + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + # Test upload + upload_logfile(trace_id=123) + + # Verify + mock_client.api.v4.upload_logfile.assert_called_once() + assert not os.path.exists(os.path.join(os.getcwd(), LOGFILE_NAME)) + +def test_upload_logfile_nonexistent_file(): + """Test that upload_logfile handles nonexistent log file gracefully""" + with patch('agentops.get_client') as mock_get_client: + upload_logfile(trace_id=123) + mock_get_client.assert_not_called() \ No newline at end of file