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

Allow .json or .yaml --log-config files #665

Merged
merged 13 commits into from
Aug 18, 2020
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ Moreover, "optional extras" means that:
- the websocket protocol will be handled by `websockets` (should you want to use `wsproto` you'd need to install it manually) if possible.
- the `--reloader` flag in development mode will use `watchgod`.
- windows users will have `colorama` installed for the colored logs.
- `python-dotenv` will be install should you want to use the `--env-file` option.

- `python-dotenv` will be installed should you want to use the `--env-file` option.
- `PyYAML` will be installed to allow you to provide a `.yaml` file to `--log-config`, if desired.

Create an application, in `example.py`:

```python
Expand Down
1 change: 1 addition & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Options:
application interface. [default: auto]
--env-file PATH Environment configuration file.
--log-config PATH Logging configuration file.
Supported formats (.ini, .json, .yaml)
--log-level [critical|error|warning|info|debug|trace]
Log level. [default: info]
--access-log / --no-access-log Enable/Disable access log.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Options:
application interface. [default: auto]
--env-file PATH Environment configuration file.
--log-config PATH Logging configuration file.
Supported formats (.ini, .json, .yaml)
--log-level [critical|error|warning|info|debug|trace]
Log level. [default: info]
--access-log / --no-access-log Enable/Disable access log.
Expand Down
6 changes: 4 additions & 2 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ $ pip install uvicorn[watchgodreload]

## Logging

* `--log-config <path>` - Logging configuration file.
* `--log-config <path>` - Logging configuration file. **Options:** *`dictConfig()` formats: .json, .yaml*. Any other format will be processed with `fileConfig()`. Set the `formatters.default.use_colors` and `formatters.access.use_colors` values to override the auto-detected behavior.
euri10 marked this conversation as resolved.
Show resolved Hide resolved
* If you wish to use a YAML file for your logging config, you will need to include PyYAML as a dependency for your project or install uvicorn with the `[standard]` optional extras.
* `--log-level <str>` - Set the log level. **Options:** *'critical', 'error', 'warning', 'info', 'debug', 'trace'.* **Default:** *'info'*.
* `--no-access-log` - Disable access log only, without changing log level.
* `--use-colors / --no-use-colors` - Enable / disable colorized formatting of the log records, in case this is not set it will be auto-detected.
* `--use-colors / --no-use-colors` - Enable / disable colorized formatting of the log records, in case this is not set it will be auto-detected. This option is ignored if the `--log-config` CLI option is used.


## Implementation

Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ websockets==8.*
wsproto==0.15.*
watchgod>=0.6,<0.7
python_dotenv==0.13.*
PyYAML>=5.1

# Packaging
twine
Expand All @@ -22,6 +23,7 @@ flake8
isort
pytest
pytest-cov
pytest-mock
requests
seed-isort-config
mypy
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_packages(package):
minimal_requirements = [
"click==7.*",
"h11>=0.8,<0.10",
"typing-extensions;" + env_marker_below_38
"typing-extensions;" + env_marker_below_38,
]

extra_requirements = [
Expand All @@ -56,6 +56,7 @@ def get_packages(package):
"colorama>=0.4.*;" + env_marker_win,
"watchgod>=0.6,<0.7",
"python-dotenv==0.13.*",
"PyYAML>=5.1",
]


Expand Down
96 changes: 94 additions & 2 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
import json
import socket
from copy import deepcopy

import pytest
import yaml

from uvicorn import protocols
from uvicorn.config import Config
from uvicorn.config import LOGGING_CONFIG, Config
from uvicorn.middleware.debug import DebugMiddleware
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
from uvicorn.middleware.wsgi import WSGIMiddleware


@pytest.fixture
def mocked_logging_config_module(mocker):
return mocker.patch("logging.config")


@pytest.fixture
def logging_config():
return deepcopy(LOGGING_CONFIG)


@pytest.fixture
def json_logging_config(logging_config):
return json.dumps(logging_config)


@pytest.fixture
def yaml_logging_config(logging_config):
return yaml.dump(logging_config)


async def asgi_app():
pass # pragma: nocover

Expand Down Expand Up @@ -77,9 +100,78 @@ async def asgi(receive, send):


@pytest.mark.parametrize(
"app, expected_interface", [(asgi_app, "3.0",), (asgi2_app, "2.0",)]
"app, expected_interface", [(asgi_app, "3.0"), (asgi2_app, "2.0")]
)
def test_asgi_version(app, expected_interface):
config = Config(app=app)
config.load()
assert config.asgi_version == expected_interface


@pytest.mark.parametrize(
"use_colors, expected",
[
pytest.param(None, None, id="use_colors_not_provided"),
pytest.param(True, True, id="use_colors_enabled"),
pytest.param(False, False, id="use_colors_disabled"),
pytest.param("invalid", False, id="use_colors_invalid_value"),
],
)
euri10 marked this conversation as resolved.
Show resolved Hide resolved
def test_log_config_default(mocked_logging_config_module, use_colors, expected):
"""
Test that one can specify the use_colors option when using the default logging
config.
"""
config = Config(app=asgi_app, use_colors=use_colors)
config.load()

mocked_logging_config_module.dictConfig.assert_called_once_with(LOGGING_CONFIG)

((provided_dict_config,), _,) = mocked_logging_config_module.dictConfig.call_args
assert provided_dict_config["formatters"]["default"]["use_colors"] == expected


def test_log_config_json(
mocked_logging_config_module, logging_config, json_logging_config, mocker
):
"""
Test that one can load a json config from disk.
"""
mocked_open = mocker.patch(
"uvicorn.config.open", mocker.mock_open(read_data=json_logging_config)
)

config = Config(app=asgi_app, log_config="log_config.json")
config.load()

mocked_open.assert_called_once_with("log_config.json")
mocked_logging_config_module.dictConfig.assert_called_once_with(logging_config)


def test_log_config_yaml(
mocked_logging_config_module, logging_config, yaml_logging_config, mocker
):
"""
Test that one can load a yaml config from disk.
"""
mocked_open = mocker.patch(
"uvicorn.config.open", mocker.mock_open(read_data=yaml_logging_config)
)

config = Config(app=asgi_app, log_config="log_config.yaml")
config.load()

mocked_open.assert_called_once_with("log_config.yaml")
mocked_logging_config_module.dictConfig.assert_called_once_with(logging_config)


def test_log_config_file(mocked_logging_config_module):
"""
Test that one can load a configparser config from disk.
"""
config = Config(app=asgi_app, log_config="log_config")
config.load()

mocked_logging_config_module.fileConfig.assert_called_once_with(
"log_config", disable_existing_loggers=False
)
19 changes: 19 additions & 0 deletions uvicorn/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import inspect
import json
import logging
import logging.config
import os
Expand All @@ -10,6 +11,14 @@

import click

try:
import yaml
except ImportError:
# If the code below that depends on yaml is exercised, it will raise a NameError.
# Install the PyYAML package or the uvicorn[standard] optional dependencies to
# enable this functionality.
pass

from uvicorn.importer import ImportFromStringError, import_from_string
from uvicorn.middleware.asgi2 import ASGI2Middleware
from uvicorn.middleware.debug import DebugMiddleware
Expand Down Expand Up @@ -221,7 +230,17 @@ def configure_logging(self):
"use_colors"
] = self.use_colors
logging.config.dictConfig(self.log_config)
elif self.log_config.endswith(".json"):
with open(self.log_config) as file:
loaded_config = json.load(file)
logging.config.dictConfig(loaded_config)
elif self.log_config.endswith(".yaml"):
with open(self.log_config) as file:
loaded_config = yaml.safe_load(file)
logging.config.dictConfig(loaded_config)
else:
# See the note about fileConfig() here:
# https://docs.python.org/3/library/logging.config.html#configuration-file-format
logging.config.fileConfig(
self.log_config, disable_existing_loggers=False
)
Expand Down