Skip to content

Commit cb91bf3

Browse files
majortisnik
authored andcommitted
RSPEED-2472: Fix uvicorn ignoring LIGHTSPEED_STACK_LOG_LEVEL env var
start_uvicorn() hardcoded log_level to INFO, so setting LIGHTSPEED_STACK_LOG_LEVEL=DEBUG or using --verbose had no effect on uvicorn's log output. Read the env var the same way log.py and lightspeed_stack.py already do. Signed-off-by: Major Hayden <major@redhat.com>
1 parent ec2f65d commit cb91bf3

File tree

2 files changed

+85
-8
lines changed

2 files changed

+85
-8
lines changed

src/runners/uvicorn.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,50 @@
11
"""Uvicorn runner."""
22

3-
from logging import INFO
3+
import logging
4+
import os
45

56
import uvicorn
67

8+
from constants import DEFAULT_LOG_LEVEL, LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR
79
from log import get_logger
810
from models.config import ServiceConfiguration
911

1012
logger = get_logger(__name__)
1113

1214

13-
def start_uvicorn(configuration: ServiceConfiguration) -> None:
15+
def _resolve_log_level() -> int:
16+
"""Resolve the uvicorn log level from the environment.
17+
18+
Reads the LIGHTSPEED_STACK_LOG_LEVEL environment variable and converts it
19+
to a Python logging level constant. Falls back to the default log level
20+
when the variable is unset or contains an invalid value.
21+
22+
Returns:
23+
The resolved logging level as an integer constant.
1424
"""
15-
Start the Uvicorn server using the provided service configuration.
25+
level_str = os.environ.get(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, DEFAULT_LOG_LEVEL)
26+
level = getattr(logging, level_str.upper(), None)
27+
if not isinstance(level, int):
28+
logger.warning(
29+
"Invalid log level '%s', falling back to %s",
30+
level_str,
31+
DEFAULT_LOG_LEVEL,
32+
)
33+
level = getattr(logging, DEFAULT_LOG_LEVEL)
34+
return level
35+
36+
37+
def start_uvicorn(configuration: ServiceConfiguration) -> None:
38+
"""Start the Uvicorn server using the provided service configuration.
1639
1740
Parameters:
1841
configuration (ServiceConfiguration): Configuration providing host,
1942
port, workers, and `tls_config` (including `tls_key_path`,
2043
`tls_certificate_path`, and `tls_key_password`). TLS fields may be None
2144
and will be forwarded to uvicorn.run as provided.
2245
"""
23-
logger.info("Starting Uvicorn")
24-
25-
log_level = INFO
46+
log_level = _resolve_log_level()
47+
logger.info("Starting Uvicorn with log level %s", logging.getLevelName(log_level))
2648

2749
# please note:
2850
# TLS fields can be None, which means we will pass those values as None to uvicorn.run

tests/unit/runners/test_uvicorn_runner.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""Unit tests for the Uvicorn runner implementation."""
22

3+
import logging
34
from pathlib import Path
4-
from pytest_mock import MockerFixture
55

6+
import pytest
7+
from pytest_mock import MockerFixture
68

9+
from constants import LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR
710
from models.config import ServiceConfiguration, TLSConfiguration
8-
from runners.uvicorn import start_uvicorn
11+
from runners.uvicorn import _resolve_log_level, start_uvicorn
912

1013

1114
def test_start_uvicorn(mocker: MockerFixture) -> None:
@@ -127,3 +130,55 @@ def test_start_uvicorn_with_root_path(mocker: MockerFixture) -> None:
127130
use_colors=True,
128131
access_log=True,
129132
)
133+
134+
135+
@pytest.mark.parametrize(
136+
("env_value", "expected_level"),
137+
[
138+
("DEBUG", logging.DEBUG),
139+
("debug", logging.DEBUG),
140+
("INFO", logging.INFO),
141+
("WARNING", logging.WARNING),
142+
("ERROR", logging.ERROR),
143+
("BOGUS", logging.INFO),
144+
],
145+
)
146+
def test_resolve_log_level_from_env(
147+
monkeypatch: pytest.MonkeyPatch, env_value: str, expected_level: int
148+
) -> None:
149+
"""Test that _resolve_log_level resolves env var values to logging constants."""
150+
monkeypatch.setenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, env_value)
151+
assert _resolve_log_level() == expected_level
152+
153+
154+
def test_resolve_log_level_defaults_to_info(
155+
monkeypatch: pytest.MonkeyPatch,
156+
) -> None:
157+
"""Test that _resolve_log_level falls back to INFO when the env var is unset."""
158+
monkeypatch.delenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, raising=False)
159+
assert _resolve_log_level() == logging.INFO
160+
161+
162+
def test_start_uvicorn_respects_debug_log_level(
163+
mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch
164+
) -> None:
165+
"""Test that start_uvicorn passes the DEBUG log level to uvicorn.run."""
166+
monkeypatch.setenv(LIGHTSPEED_STACK_LOG_LEVEL_ENV_VAR, "DEBUG")
167+
configuration = ServiceConfiguration(
168+
host="localhost", port=8080, workers=1
169+
) # pyright: ignore[reportCallIssue]
170+
171+
mocked_run = mocker.patch("uvicorn.run")
172+
start_uvicorn(configuration)
173+
mocked_run.assert_called_once_with(
174+
"app.main:app",
175+
host="localhost",
176+
port=8080,
177+
workers=1,
178+
log_level=logging.DEBUG,
179+
ssl_certfile=None,
180+
ssl_keyfile=None,
181+
ssl_keyfile_password="",
182+
use_colors=True,
183+
access_log=True,
184+
)

0 commit comments

Comments
 (0)