Files
e2b-dev--e2b/packages/python-sdk/e2b/template/logger.py
T
2026-07-13 12:47:58 +08:00

233 lines
5.9 KiB
Python

import sys
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, TypedDict, Callable, Dict, Literal
from rich.console import Console
from rich.style import Style
from rich.text import Text
from e2b.template.utils import strip_ansi_escape_codes
"""Log entry severity levels."""
LogEntryLevel = Literal["debug", "info", "warn", "error"]
@dataclass
class LogEntry:
"""
Represents a single log entry from the template build process.
"""
timestamp: datetime
level: LogEntryLevel
message: str
def __post_init__(self):
self.message = strip_ansi_escape_codes(self.message)
def __str__(self) -> str:
return f"[{self.timestamp.isoformat()}] [{self.level}] {self.message}"
@dataclass
class LogEntryStart(LogEntry):
"""
Special log entry indicating the start of a build process.
"""
level: LogEntryLevel = field(default="debug", init=False)
@dataclass
class LogEntryEnd(LogEntry):
"""
Special log entry indicating the end of a build process.
"""
level: LogEntryLevel = field(default="debug", init=False)
"""
Interval in milliseconds for updating the build timer display.
"""
TIMER_UPDATE_INTERVAL_MS = 150
"""
Default minimum log level to display.
"""
DEFAULT_LEVEL: LogEntryLevel = "info"
"""
Colored labels for each log level.
"""
levels: Dict[LogEntryLevel, tuple[str, Style]] = {
"error": ("ERROR", Style(color="red")),
"warn": ("WARN ", Style(color="#FF4400")),
"info": ("INFO ", Style(color="#FF8800")),
"debug": ("DEBUG", Style(color="bright_black")),
}
"""
Numeric ordering of log levels for comparison (lower = less severe).
"""
level_order = {
"debug": 0,
"info": 1,
"warn": 2,
"error": 3,
}
def set_interval(func, interval):
"""
Returns a stop function that can be called to cancel the interval.
Similar to JavaScript's setInterval.
:param func: Function to execute at each interval
:param interval: Interval duration in **seconds**
:return: Stop function that can be called to cancel the interval
"""
stopped = threading.Event()
def loop():
while not stopped.is_set():
if stopped.wait(interval): # wait returns True if stopped
break
if not stopped.is_set(): # Double-check before executing
func()
threading.Thread(target=loop, daemon=True).start()
return stopped.set # Return the stop function
class DefaultBuildLoggerInitialState(TypedDict):
start_time: float
animation_frame: int
timer: Optional[Callable[[], None]]
class DefaultBuildLogger:
__console = Console()
__min_level: LogEntryLevel
__state: DefaultBuildLoggerInitialState
def __init__(self, min_level: Optional[LogEntryLevel] = None):
self.__min_level = min_level if min_level is not None else DEFAULT_LEVEL
self.__reset_initial_state()
def logger(self, log):
if isinstance(log, LogEntryStart):
self.__start_timer()
return
if isinstance(log, LogEntryEnd):
if self.__state["timer"] is not None:
self.__state["timer"]()
return
# Filter by minimum level
if level_order[log.level] < level_order[self.__min_level]:
return
formatted_line = self.__format_log_line(log)
self.__console.print(formatted_line)
# Redraw the timer line
self.__update_timer()
def __reset_initial_state(self, timer: Optional[Callable[[], None]] = None):
self.__state = {
"start_time": time.time(),
"animation_frame": 0,
"timer": timer,
}
def __format_timer_line(self) -> str:
elapsed_seconds = time.time() - self.__state["start_time"]
return f"{elapsed_seconds:.1f}s"
def __animate_status(self) -> str:
frames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]
idx = self.__state["animation_frame"] % len(frames)
return frames[idx]
def __format_log_line(self, line: LogEntry) -> Text:
timer = self.__format_timer_line().ljust(5)
timestamp = line.timestamp.strftime("%H:%M:%S")
level_text, level_style = levels.get(line.level, levels[DEFAULT_LEVEL])
# Build a rich Text object
text = Text.assemble(
timer,
" | ",
(timestamp, "dim"),
" ",
(level_text, level_style),
" ",
line.message,
)
return text
def __start_timer(self):
if not sys.stdout.isatty():
return
# Start the timer interval
stop_timer = set_interval(
self.__update_timer, TIMER_UPDATE_INTERVAL_MS / 1000.0
)
self.__reset_initial_state(stop_timer)
# Initial timer display
self.__update_timer()
def __update_timer(self):
if not sys.stdout.isatty():
return
self.__state["animation_frame"] += 1
jumping_squares = self.__animate_status()
timer_text = Text.assemble(
jumping_squares, " Building ", self.__format_timer_line()
)
# Print with carriage return
self.__console.print(timer_text, end="\r")
def default_build_logger(
min_level: Optional[LogEntryLevel] = None,
) -> Callable[[LogEntry], None]:
"""
Create a default build logger with animated timer display.
:param min_level: Minimum log level to display (default: 'info')
:return: Logger function that accepts LogEntry instances
Example
```python
from e2b import Template, default_build_logger
template = Template().from_python_image()
# Use with build - implementation would be in build_async module
# await Template.build(template,
# alias='my-template',
# on_build_logs=default_build_logger(min_level='debug')
# )
```
"""
build_logger = DefaultBuildLogger(min_level)
return build_logger.logger