Skip to content
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

refactor(cluster): make bot logs display better #196

Merged
merged 4 commits into from
Feb 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions silverback/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1316,8 +1316,21 @@ def stop_bot(cluster: "ClusterClient", name: str):
help="Return logs since N ago.",
callback=timedelta_callback,
)
@click.option(
"-f",
"--follow",
help="Stream logs as they come in",
is_flag=True,
default=False,
)
@cluster_client
def show_bot_logs(cluster: "ClusterClient", name: str, log_level: str, since: timedelta | None):
def show_bot_logs(
cluster: "ClusterClient",
name: str,
log_level: str,
since: timedelta | None,
follow: bool,
):
"""Show runtime logs for BOT in CLUSTER"""

start_time = None
Expand All @@ -1332,8 +1345,8 @@ def show_bot_logs(cluster: "ClusterClient", name: str, log_level: str, since: ti
except KeyError:
level = LogLevel.INFO

for log in bot.filter_logs(log_level=level, start_time=start_time):
click.echo(log)
for log in bot.get_logs(log_level=level, start_time=start_time, follow=follow):
click.echo(str(log))


@bots.command(name="errors", section="Bot Operation Commands")
Expand Down
4 changes: 2 additions & 2 deletions silverback/cluster/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def errors(self) -> list[str]:
handle_error_with_response(response)
return response.json()

def filter_logs(
def get_logs(
self,
log_level: LogLevel = LogLevel.INFO,
start_time: datetime | None = None,
Expand All @@ -201,7 +201,7 @@ def filter_logs(

@property
def logs(self) -> list[BotLogEntry]:
return list(self.filter_logs())
return list(self.get_logs())

def remove(self):
response = self.cluster.delete(f"/bots/{self.id}")
Expand Down
54 changes: 50 additions & 4 deletions silverback/cluster/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

import enum
import math
import re
import uuid
from datetime import datetime
from typing import Annotated, Any
from typing import Annotated, Any, ClassVar

from ape.logging import LogLevel
from ape.logging import CLICK_STYLE_KWARGS, LogLevel
from ape.types import AddressType, HexBytes
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.hmac import HMAC, hashes
from eth_utils import to_bytes, to_int
from pydantic import BaseModel, Field, computed_field, field_validator
from typing_extensions import Self


def normalize_bytes(val: bytes, length: int = 16) -> bytes:
Expand Down Expand Up @@ -350,9 +352,53 @@ class BotInfo(BaseModel):


class BotLogEntry(BaseModel):
level: LogLevel = LogLevel.INFO
LOG_PATTERN: ClassVar[re.Pattern] = re.compile(
r"""^
(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?)\s
(?:
(?P<level>DEBUG:\s\s\s|INFO:\s\s\s\s|SUCCESS:\s|WARNING:\s|ERROR:\s\s\s|CRITICAL:)\s
)?
(?P<message>.*)$""",
re.VERBOSE,
)

level: LogLevel | None = None
timestamp: datetime | None = None
message: str

@classmethod
def parse_line(cls, line: str) -> Self:
# Typical line is like: `{timestamp} {str(log_level) + ':':<9} {message}`
if not (match := cls.LOG_PATTERN.match(line)):
return cls(message=line)

if level := match.group("level"):
level = LogLevel[level.strip()[:-1]]

return cls(
timestamp=match.group("timestamp"),
level=level,
message=match.group("message"),
)

def __str__(self) -> str:
return f"{self.timestamp} [{self.level}]: {self.message}"
from click import style as click_style

if self.level is not None:
styles = CLICK_STYLE_KWARGS.get(self.level, {})
level_str = click_style(f"{self.level.name:<8}", **styles) # type: ignore[arg-type]
else:
level_str = ""

if self.timestamp is not None:
timestamp_str = click_style(f"{self.timestamp:%x %X}", bold=True)
else:
timestamp_str = ""

# NOTE: Add offset (18+8+2=28) to all newlines in message after the first
if "\n" in (message := self.message):
message = (" " * 28 + "\n").join(message.split("\n"))

# NOTE: Max size of `LogLevel` is 8 chars
# NOTE: Max size of normalized timestamp is 18 chars
return f"{timestamp_str:<18} {level_str:<8} | {message}"
Loading