Skip to content

Commit

Permalink
feat(internal): Add option to for puya to output logs in json format …
Browse files Browse the repository at this point in the history
…rather than pretty printed
  • Loading branch information
tristanmenzel committed Dec 5, 2024
1 parent 7dda69f commit 8f0688c
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 7 deletions.
12 changes: 10 additions & 2 deletions src/puya/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import attrs

from puya.log import LogLevel, configure_logging
from puya.log import LogFormat, LogLevel, configure_logging
from puya.main import main


Expand All @@ -14,6 +14,7 @@ class _PuyaCLIArgs:
awst: Path | None = None
source_annotations: Path | None = None
log_level: LogLevel = LogLevel.info
log_format: LogFormat = LogFormat.default


def cli() -> None:
Expand All @@ -25,12 +26,19 @@ def cli() -> None:
parser.add_argument(
"--log-level", type=LogLevel.from_string, choices=list(LogLevel), default=LogLevel.info
)
parser.add_argument(
"--log-format",
type=LogFormat.from_string,
choices=list(LogFormat),
default=LogFormat.default,
)
parser.add_argument("--options", type=Path, required=True)
parser.add_argument("--awst", type=Path, required=True)
parser.add_argument("--source-annotations", type=Path)
parsed_args = _PuyaCLIArgs()
parser.parse_args(namespace=parsed_args)
configure_logging(min_log_level=parsed_args.log_level)

configure_logging(min_log_level=parsed_args.log_level, log_format=parsed_args.log_format)

assert parsed_args.options
options_json = parsed_args.options.read_text("utf8")
Expand Down
85 changes: 80 additions & 5 deletions src/puya/log.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import contextlib
import json
import logging
import os.path
import sys
import typing
from collections import Counter
from collections.abc import Iterator, Mapping, Sequence
from contextvars import ContextVar
from enum import IntEnum
from enum import IntEnum, StrEnum, auto
from io import StringIO
from pathlib import Path

Expand All @@ -16,6 +17,18 @@
from puya.parse import SourceLocation


class LogFormat(StrEnum):
default = auto()
json = auto()

@staticmethod
def from_string(s: str) -> "LogFormat":
try:
return LogFormat[s]
except KeyError as err:
raise ValueError from err


class LogLevel(IntEnum):
notset = logging.NOTSET
debug = logging.DEBUG
Expand Down Expand Up @@ -74,11 +87,59 @@ def exit_if_errors(self) -> None:
_current_ctx: ContextVar[LoggingContext] = ContextVar("current_ctx")


class PuyaJsonRender(structlog.dev.ConsoleRenderer):
def __init__(self, *, base_path: str) -> None:
super().__init__(colors=False)
self.base_path = base_path + os.path.sep

def _location_json(
self, location: SourceLocation | None
) -> Mapping[str, str | int | None] | None:
if not location or not location.file:
return None

file = str(location.file)
if file.startswith(self.base_path):
file = file[len(self.base_path) :]

return {
"file": file,
"line": location.line,
"end_line": location.end_line,
"column": location.column,
"end_column": location.end_column,
}

def __call__(
self,
_logger: structlog.typing.WrappedLogger,
_name: str,
event_dict: structlog.typing.EventDict,
) -> str:
# force event to str for compatibility with standard library
event = event_dict.pop("event", None)
if not isinstance(event, str):
event = str(event)

important: bool = event_dict.pop("important", False)
location: SourceLocation | None = event_dict.pop("location", None)
level = event_dict.pop("level", "info")

return json.dumps(
{
"level": level,
"location": self._location_json(location),
"message": event,
"important": important,
}
)


class PuyaConsoleRender(structlog.dev.ConsoleRenderer):
def __init__(self, *, colors: bool):
def __init__(self, *, colors: bool, base_path: str):
super().__init__(colors=colors)
self.level_to_color = self.get_default_level_styles(colors)
self.base_path = str(Path.cwd()) # TODO: don't assume this?
self.base_path = base_path
if not self.base_path.endswith(
os.path.sep
): # TODO: can we always append the path seperator?
Expand Down Expand Up @@ -213,17 +274,31 @@ def __call__(


def configure_logging(
*, min_log_level: LogLevel = LogLevel.notset, cache_logger: bool = True
*,
min_log_level: LogLevel = LogLevel.notset,
cache_logger: bool = True,
log_format: LogFormat = LogFormat.default,
) -> None:
if cache_logger and structlog.is_configured():
raise ValueError(
"Logging can not be configured more than once if using cache_logger = True"
)
base_path = str(Path.cwd()) # TODO: don't assume this?
match log_format:
case LogFormat.json:
log_renderer: structlog.typing.Processor = PuyaJsonRender(base_path=base_path)
case LogFormat.default:
log_renderer = PuyaConsoleRender(
colors="NO_COLOR" not in os.environ, base_path=base_path
)
case never:
typing.assert_never(never)

processors: list[structlog.typing.Processor] = [
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.StackInfoRenderer(),
PuyaConsoleRender(colors="NO_COLOR" not in os.environ),
log_renderer,
]
if min_log_level != LogLevel.notset:
# filtering via a processor instead of via the logger like
Expand Down

0 comments on commit 8f0688c

Please sign in to comment.