Skip to content
Open
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
1 change: 1 addition & 0 deletions news/+eng-10963-log-pipeline.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Framework logging now flows through standard python `logging` with per-module loggers (`reflex_base.utils.log`, re-exported as `reflex.utils.log`), bootstrapped on `import reflex`. Rich colored output is preserved at the sink, and `REFLEX_LOG_JSON` emits machine-readable JSON-lines records. `--loglevel critical` no longer prints the system-info banner (broken `LogLevel` string-compare ordering fixed).
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `reflex_base.utils.log`: a standard python logging pipeline with a rich-rendering console handler (legacy colors preserved), a JSON-lines handler behind `REFLEX_LOG_JSON`, record deduplication, and file logging. `LogLevel` gained a correct total ordering and `to_logging_level()`, and the interactive console helpers (`print`/`rule`/`status`/`progress`) now respect JSON mode.
60 changes: 58 additions & 2 deletions packages/reflex-base/src/reflex_base/constants/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import logging
import platform
from enum import Enum
from importlib import metadata
Expand Down Expand Up @@ -248,6 +249,19 @@ def from_string(cls, level: str | None) -> LogLevel | None:
except KeyError:
return None

# The str mixin supplies alphabetical comparisons, so all four operators
# must be overridden to compare by verbosity rank instead.
def __lt__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is less verbose than the other log level.
"""
return _LOG_LEVEL_RANK[self] < _LOG_LEVEL_RANK[other]

def __le__(self, other: LogLevel) -> bool:
"""Compare log levels.

Expand All @@ -257,8 +271,39 @@ def __le__(self, other: LogLevel) -> bool:
Returns:
True if the log level is less than or equal to the other log level.
"""
levels = list(LogLevel)
return levels.index(self) <= levels.index(other)
return _LOG_LEVEL_RANK[self] <= _LOG_LEVEL_RANK[other]

def __gt__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is more verbose-restrictive than the other.
"""
return _LOG_LEVEL_RANK[self] > _LOG_LEVEL_RANK[other]

def __ge__(self, other: LogLevel) -> bool:
"""Compare log levels.

Args:
other: The other log level.

Returns:
True if the log level is greater than or equal to the other.
"""
return _LOG_LEVEL_RANK[self] >= _LOG_LEVEL_RANK[other]

def to_logging_level(self) -> int:
"""Map this level to a stdlib logging level number.

DEFAULT acts as a threshold equivalent to INFO.

Returns:
The stdlib logging level.
"""
return _LOGGING_LEVELS[self]

def subprocess_level(self):
"""Return the log level for the subprocess.
Expand All @@ -269,6 +314,17 @@ def subprocess_level(self):
return self if self != LogLevel.DEFAULT else LogLevel.WARNING


_LOG_LEVEL_RANK = {level: rank for rank, level in enumerate(LogLevel)}
_LOGGING_LEVELS = {
LogLevel.DEBUG: logging.DEBUG,
LogLevel.DEFAULT: logging.INFO,
LogLevel.INFO: logging.INFO,
LogLevel.WARNING: logging.WARNING,
LogLevel.ERROR: logging.ERROR,
LogLevel.CRITICAL: logging.CRITICAL,
}


# Server socket configuration variables
POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000

Expand Down
10 changes: 7 additions & 3 deletions packages/reflex-base/src/reflex_base/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import dataclasses
import enum
import importlib
import logging
import os
from collections.abc import Sequence
from functools import lru_cache
Expand All @@ -27,6 +28,8 @@
from reflex_base.utils.exceptions import EnvironmentVarValueError
from reflex_base.utils.types import GenericType, is_union, value_inside_optional

logger = logging.getLogger(__name__)


def get_default_value_for_field(field: dataclasses.Field) -> Any:
"""Get the default value for a field.
Expand Down Expand Up @@ -705,6 +708,9 @@ class EnvironmentVariables:
# Enable full logging of debug messages to reflex user directory.
REFLEX_ENABLE_FULL_LOGGING: EnvVar[bool] = env_var(False)

# Emit logs as machine-readable JSON records instead of rich console output.
REFLEX_LOG_JSON: EnvVar[bool] = env_var(False)

# Whether to enable hot module replacement
VITE_HMR: EnvVar[bool] = env_var(True)

Expand Down Expand Up @@ -781,13 +787,11 @@ def _load_dotenv_from_files(files: list[Path]):
Args:
files: A list of Path objects representing the environment variable files.
"""
from reflex_base.utils import console

if not files:
return

if load_dotenv is None:
console.error(
logger.error(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This error now goes through a plain logging.getLogger(__name__) call instead of console.error(). If reflex_base.environment is imported before reflex (so log.bootstrap() hasn't claimed this logger yet), the record falls back to default stdlib handling — plain stderr text instead of a JSON record, and it bypasses full-logging file capture — breaking the logging contract this PR establishes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/environment.py, line 794:

<comment>This error now goes through a plain `logging.getLogger(__name__)` call instead of `console.error()`. If `reflex_base.environment` is imported before `reflex` (so `log.bootstrap()` hasn't claimed this logger yet), the record falls back to default stdlib handling — plain stderr text instead of a JSON record, and it bypasses full-logging file capture — breaking the logging contract this PR establishes.</comment>

<file context>
@@ -781,13 +787,11 @@ def _load_dotenv_from_files(files: list[Path]):
 
     if load_dotenv is None:
-        console.error(
+        logger.error(
             """The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
         )
</file context>

"""The `python-dotenv` package is required to load environment variables from a file. Run `pip install "python-dotenv>=1.1.0"`."""
)
Comment on lines +794 to 796

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Bootstrap missing for dotenv errors

If REFLEX_ENV_FILE is set, python-dotenv is unavailable, and reflex_base.environment is imported without first importing reflex, this error uses an unclaimed stdlib logger. It emits plain stderr instead of a JSON record and bypasses Reflex file logging, breaking the configured logging contract.

Knowledge Base Used: Config and Environment System

return
Expand Down
46 changes: 36 additions & 10 deletions packages/reflex-base/src/reflex_base/utils/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
import datetime
import functools
import inspect
import os
import shutil
import sys
import time
from collections.abc import Sequence
from pathlib import Path
from types import FrameType, ModuleType

from rich.console import Console
from rich.progress import MofNCompleteColumn, Progress, TaskID, TimeElapsedColumn
from rich.prompt import Prompt
from rich.table import Table

from reflex_base.constants import LogLevel
from reflex_base.constants.base import Reflex
from reflex_base.utils import log as _log
from reflex_base.utils.decorator import once

# Console for pretty printing.
Expand Down Expand Up @@ -58,19 +60,11 @@ def set_log_level(log_level: LogLevel | None):

Args:
log_level: The log level to set.

Raises:
TypeError: If the log level is a string.
"""
if log_level is None:
return
if not isinstance(log_level, LogLevel):
msg = f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead."
raise TypeError(msg)
_log.set_log_level(log_level)
global _LOG_LEVEL
if log_level != _LOG_LEVEL:
# Set the loglevel persistenly for subprocesses.
os.environ["REFLEX_LOGLEVEL"] = log_level.value
_LOG_LEVEL = log_level


Expand All @@ -91,6 +85,9 @@ def print(msg: str, *, dedupe: bool = False, **kwargs):
dedupe: If True, suppress multiple console logs of print message.
kwargs: Keyword arguments to pass to the print function.
"""
if _log.is_json_mode():
_log.emit_json_print(msg, dedupe=dedupe)
return
Comment on lines +88 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route every console output path through JSON mode

When REFLEX_LOG_JSON=true, this guard only covers console.print(): common flows such as reflex init call console.log(), which writes a timestamped Rich line directly through _console.log, and print_table() similarly writes rendered text directly. Those lines are not JSON objects, so a JSON-lines parser fails even though the mode promises machine-readable output; make all direct console writers JSON-aware or suppress them.

Useful? React with 👍 / 👎.

if dedupe:
if msg in _EMITTED_PRINTS:
return
Expand All @@ -106,6 +103,9 @@ def _print_stderr(msg: str, *, dedupe: bool = False, **kwargs):
dedupe: If True, suppress multiple console logs of print message.
kwargs: Keyword arguments to pass to the print function.
"""
if _log.is_json_mode():
_log.emit_json_print(msg, dedupe=dedupe, stderr=True)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: In JSON mode, console.error(...) (and console.warn(...)) now route through emit_json_print(...), which hardcodes "level": "info" in the emitted record. So real errors and warnings — precisely the records a machine consumer of the JSON stream most needs to distinguish — are tagged as info. The stderr=True flag only selects the stream; the severity is lost. Consider passing an explicit level (e.g. error/warning) into emit_json_print based on the caller, so the JSON records preserve the legacy console severity.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/utils/console.py, line 107:

<comment>In JSON mode, `console.error(...)` (and `console.warn(...)`) now route through `emit_json_print(...)`, which hardcodes `"level": "info"` in the emitted record. So real errors and warnings — precisely the records a machine consumer of the JSON stream most needs to distinguish — are tagged as `info`. The `stderr=True` flag only selects the stream; the severity is lost. Consider passing an explicit level (e.g. `error`/`warning`) into `emit_json_print` based on the caller, so the JSON records preserve the legacy console severity.</comment>

<file context>
@@ -106,6 +103,9 @@ def _print_stderr(msg: str, *, dedupe: bool = False, **kwargs):
         kwargs: Keyword arguments to pass to the print function.
     """
+    if _log.is_json_mode():
+        _log.emit_json_print(msg, dedupe=dedupe, stderr=True)
+        return
     if dedupe:
</file context>

return
if dedupe:
if msg in _EMITTED_PRINTS:
return
Expand Down Expand Up @@ -241,6 +241,8 @@ def rule(title: str, **kwargs):
title: The title of the rule.
kwargs: Keyword arguments to pass to the print function.
"""
if _log.is_json_mode():
return
_console.rule(title, **kwargs)


Expand Down Expand Up @@ -417,6 +419,27 @@ def ask(
)


def print_table(
tabular_data: list[list[str]],
headers: Sequence[str] = (),
) -> None:
"""Print a table to the console.

Args:
tabular_data: The data to print in tabular format.
headers: The headers for the table.
"""
table = Table()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: print_table corrupts JSON-mode stdout with Rich table text; suppress or serialize it when REFLEX_LOG_JSON is enabled, as rule already does.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-base/src/reflex_base/utils/console.py, line 432:

<comment>`print_table` corrupts JSON-mode stdout with Rich table text; suppress or serialize it when `REFLEX_LOG_JSON` is enabled, as `rule` already does.</comment>

<file context>
@@ -417,6 +419,27 @@ def ask(
+        tabular_data: The data to print in tabular format.
+        headers: The headers for the table.
+    """
+    table = Table()
+
+    for column in headers:
</file context>
Suggested change
table = Table()
if _log.is_json_mode():
return
table = Table()


for column in headers:
table.add_column(column)

for row in tabular_data:
table.add_row(*row)

_console.print(table)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Table bypasses JSON mode

The new print_table helper writes Rich output unconditionally, unlike the neighboring console helpers that emit JSON or remain silent in JSON mode. Callers using it with REFLEX_LOG_JSON therefore add non-JSON text to the machine-readable stream and must implement their own special-case guard.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!



def progress():
"""Create a new progress bar.

Expand All @@ -427,6 +450,7 @@ def progress():
*Progress.get_default_columns()[:-1],
MofNCompleteColumn(),
TimeElapsedColumn(),
disable=_log.is_json_mode(),
)


Expand All @@ -440,6 +464,8 @@ def status(*args, **kwargs):
Returns:
A new status.
"""
if _log.is_json_mode():
return _log._quiet_console.status(*args, **kwargs)
return _console.status(*args, **kwargs)


Expand Down
Loading
Loading