diff --git a/.env.example b/.env.example index 82d2a45fd..7b3c76e9b 100644 --- a/.env.example +++ b/.env.example @@ -51,6 +51,10 @@ LANGFLOW_LOG_LEVEL= # Example: LANGFLOW_LOG_FILE=logs/langflow.log LANGFLOW_LOG_FILE= +# Time/Size for log to rotate +# Example: LANGFLOW_LOG_ROTATION=‘10 MB’/‘1 day’ +LANGFLOW_LOG_ROTATION= + # Path to the frontend directory containing build files # Example: LANGFLOW_FRONTEND_PATH=/path/to/frontend/build/files LANGFLOW_FRONTEND_PATH= diff --git a/docs/docs/Develop/logging.mdx b/docs/docs/Develop/logging.mdx index 5a160fb5c..4c9cf1c02 100644 --- a/docs/docs/Develop/logging.mdx +++ b/docs/docs/Develop/logging.mdx @@ -40,6 +40,12 @@ For example, `LANGFLOW_LOG_FILE=path/to/logfile.log`. * `LANGFLOW_LOG_ENV=container_csv`: Outputs CSV-formatted plain text to stdout. * `LANGFLOW_LOG_ENV=default` or unset: Outputs prettified output with [RichHandler](https://rich.readthedocs.io/en/stable/reference/logging.html). +* `LANGFLOW_LOG_ROTATION` controls when the log file is rotated, either based on time or file size. By default, logs are rotated every 1 day. + + * Time-based rotation: "1 day", "12 hours", "1 week" + * Size-based rotation: "10 MB", "1 GB" + * Disable rotation: "None" (log files will grow without limit) + A complete example `.env` file is available in the [Langflow repository](https://github.com/langflow-ai/langflow/blob/main/.env.example). ## Flow and component logs diff --git a/src/backend/base/langflow/__main__.py b/src/backend/base/langflow/__main__.py index 43084df20..470c133af 100644 --- a/src/backend/base/langflow/__main__.py +++ b/src/backend/base/langflow/__main__.py @@ -188,6 +188,7 @@ def run( show_default=False, ), log_file: Path | None = typer.Option(None, help="Path to the log file.", show_default=False), + log_rotation: str | None = typer.Option(None, help="Log rotation(Time/Size).", show_default=False), cache: str | None = typer.Option( # noqa: ARG001 None, help="Type of cache to use. (InMemoryCache, SQLiteCache)", @@ -263,7 +264,7 @@ def run( else: os.environ["LANGFLOW_LOG_LEVEL"] = env_log_level.lower() - configure(log_level=log_level, log_file=log_file) + configure(log_level=log_level, log_file=log_file, log_rotation=log_rotation) # Create progress indicator (show verbose timing if log level is DEBUG) verbose = log_level == "debug" diff --git a/src/backend/base/langflow/load/load.py b/src/backend/base/langflow/load/load.py index 502195e12..6c9f84f1b 100644 --- a/src/backend/base/langflow/load/load.py +++ b/src/backend/base/langflow/load/load.py @@ -21,6 +21,7 @@ async def aload_flow_from_json( tweaks: dict | None = None, log_level: str | None = None, log_file: str | None = None, + log_rotation: str | None = None, env_file: str | None = None, cache: str | None = None, disable_logs: bool | None = True, @@ -33,6 +34,7 @@ async def aload_flow_from_json( tweaks (Optional[dict]): Optional tweaks to apply to the loaded flow graph. log_level (Optional[str]): Optional log level to configure for the flow processing. log_file (Optional[str]): Optional log file to configure for the flow processing. + log_rotation (Optional[str]): Optional log rotation(Time/Size) to configure for the flow processing. env_file (Optional[str]): Optional .env file to override environment variables. cache (Optional[str]): Optional cache path to update the flow settings. disable_logs (Optional[bool], default=True): Optional flag to disable logs during flow processing. @@ -47,7 +49,9 @@ async def aload_flow_from_json( """ # If input is a file path, load JSON from the file log_file_path = Path(log_file) if log_file else None - configure(log_level=log_level, log_file=log_file_path, disable=disable_logs, async_file=True) + configure( + log_level=log_level, log_file=log_file_path, disable=disable_logs, async_file=True, log_rotation=log_rotation + ) # override env variables with .env file if env_file and tweaks is not None: @@ -83,6 +87,7 @@ def load_flow_from_json( tweaks: dict | None = None, log_level: str | None = None, log_file: str | None = None, + log_rotation: str | None = None, env_file: str | None = None, cache: str | None = None, disable_logs: bool | None = True, @@ -95,6 +100,7 @@ def load_flow_from_json( tweaks (Optional[dict]): Optional tweaks to apply to the loaded flow graph. log_level (Optional[str]): Optional log level to configure for the flow processing. log_file (Optional[str]): Optional log file to configure for the flow processing. + log_rotation (Optional[str]): Optional log rotation(Time/Size) to configure for the flow processing. env_file (Optional[str]): Optional .env file to override environment variables. cache (Optional[str]): Optional cache path to update the flow settings. disable_logs (Optional[bool], default=True): Optional flag to disable logs during flow processing. @@ -113,6 +119,7 @@ def load_flow_from_json( tweaks=tweaks, log_level=log_level, log_file=log_file, + log_rotation=log_rotation, env_file=env_file, cache=cache, disable_logs=disable_logs, @@ -131,6 +138,7 @@ async def arun_flow_from_json( output_component: str | None = None, log_level: str | None = None, log_file: str | None = None, + log_rotation: str | None = None, env_file: str | None = None, cache: str | None = None, disable_logs: bool | None = True, @@ -148,6 +156,7 @@ async def arun_flow_from_json( output_component (Optional[str], optional): The specific component to output. Defaults to None. log_level (Optional[str], optional): The log level to use. Defaults to None. log_file (Optional[str], optional): The log file to write logs to. Defaults to None. + log_rotation (Optional[str], optional): The log rotation to use. Defaults to None. env_file (Optional[str], optional): The environment file to load. Defaults to None. cache (Optional[str], optional): The cache directory to use. Defaults to None. disable_logs (Optional[bool], optional): Whether to disable logs. Defaults to True. @@ -165,6 +174,7 @@ async def arun_flow_from_json( tweaks=tweaks, log_level=log_level, log_file=log_file, + log_rotation=log_rotation, env_file=env_file, cache=cache, disable_logs=disable_logs, @@ -193,6 +203,7 @@ def run_flow_from_json( output_component: str | None = None, log_level: str | None = None, log_file: str | None = None, + log_rotation: str | None = None, env_file: str | None = None, cache: str | None = None, disable_logs: bool | None = True, @@ -214,6 +225,7 @@ def run_flow_from_json( output_component (Optional[str], optional): The specific component to output. Defaults to None. log_level (Optional[str], optional): The log level to use. Defaults to None. log_file (Optional[str], optional): The log file to write logs to. Defaults to None. + log_rotation (Optional[str], optional): The log rotation to use. Defaults to None. env_file (Optional[str], optional): The environment file to load. Defaults to None. cache (Optional[str], optional): The cache directory to use. Defaults to None. disable_logs (Optional[bool], optional): Whether to disable logs. Defaults to True. @@ -234,6 +246,7 @@ def run_flow_from_json( output_component=output_component, log_level=log_level, log_file=log_file, + log_rotation=log_rotation, env_file=env_file, cache=cache, disable_logs=disable_logs, diff --git a/src/backend/base/langflow/logging/logger.py b/src/backend/base/langflow/logging/logger.py index 662fb601b..496389312 100644 --- a/src/backend/base/langflow/logging/logger.py +++ b/src/backend/base/langflow/logging/logger.py @@ -1,4 +1,3 @@ -import asyncio import json import logging import os @@ -9,10 +8,7 @@ from threading import Lock, Semaphore from typing import TypedDict import orjson -from loguru import _defaults, logger -from loguru._error_interceptor import ErrorInterceptor -from loguru._file_sink import FileSink -from loguru._simple_sinks import AsyncSink +from loguru import logger from platformdirs import user_cache_dir from rich.logging import RichHandler from typing_extensions import NotRequired, override @@ -154,24 +150,6 @@ class LogConfig(TypedDict): log_format: NotRequired[str] -class AsyncFileSink(AsyncSink): - def __init__(self, file): - self._sink = FileSink( - path=file, - rotation="10 MB", # Log rotation based on file size - delay=True, - ) - super().__init__(self.write_async, None, ErrorInterceptor(_defaults.LOGURU_CATCH, -1)) - - async def complete(self): - await asyncio.to_thread(self._sink.stop) - for task in self._tasks: - await self._complete_task(task) - - async def write_async(self, message): - await asyncio.to_thread(self._sink.write, message) - - def is_valid_log_format(format_string) -> bool: """Validates a logging format string by attempting to format it with a dummy LogRecord. @@ -204,6 +182,7 @@ def configure( log_env: str | None = None, log_format: str | None = None, async_file: bool = False, + log_rotation: str | None = None, ) -> None: if disable and log_level is None and log_file is None: logger.disable("langflow") @@ -252,12 +231,20 @@ def configure( logger.debug(f"Cache directory: {cache_dir}") log_file = cache_dir / "langflow.log" logger.debug(f"Log file: {log_file}") + + if os.getenv("LANGFLOW_LOG_ROTATION") and log_rotation is None: + log_rotation = os.getenv("LANGFLOW_LOG_ROTATION") + elif log_rotation is None: + log_rotation = "1 day" + try: logger.add( - sink=AsyncFileSink(log_file) if async_file else log_file, + sink=log_file, level=log_level.upper(), format=log_format, serialize=True, + enqueue=async_file, + rotation=log_rotation, ) except Exception: # noqa: BLE001 logger.exception("Error setting up log file") diff --git a/src/backend/base/langflow/services/flow/flow_runner.py b/src/backend/base/langflow/services/flow/flow_runner.py index 45d4d513a..46a2a7f7b 100644 --- a/src/backend/base/langflow/services/flow/flow_runner.py +++ b/src/backend/base/langflow/services/flow/flow_runner.py @@ -46,12 +46,19 @@ class LangflowRunnerExperimental: should_initialize_db: bool = True, log_level: str | None = None, log_file: str | None = None, + log_rotation: str | None = None, disable_logs: bool = False, async_log_file: bool = True, ): self.should_initialize_db = should_initialize_db log_file_path = Path(log_file) if log_file else None - configure(log_level=log_level, log_file=log_file_path, disable=disable_logs, async_file=async_log_file) + configure( + log_level=log_level, + log_file=log_file_path, + log_rotation=log_rotation, + disable=disable_logs, + async_file=async_log_file, + ) async def run( self,