Skip to content

Commit

Permalink
refactor(cluster): make bot logs display better (#196)
Browse files Browse the repository at this point in the history
* refactor(cluster): change `filter_logs` to `get_logs`

* refactor(cli): add `--follow` to `bots logs` command

* fix(cluster): fix how to render bot log entries

* feat(cluster): add classmethod to parse BotLogEntry from logging line
  • Loading branch information
fubuloubu authored Feb 9, 2025
1 parent a0f2ead commit 399007f
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 9 deletions.
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}"

0 comments on commit 399007f

Please sign in to comment.