Skip to content

Commit d570e77

Browse files
authored
Low-lift solution to move to stdlib logging (#611)
* low-lift solution move to stdlib * reverts * more directly match old behaviour * change err to warn * one msg log for root warning * remove unnecessary logging code * green log for all non-err handled reqs * logout is not a warning * Support stdlib logging `extra` parameter data forwarding * spacing * accurate comment * remove legacy chatlog logging to file * simplify * file logging without colors (for the sanity of humanity) * move debug query logging * sql query useful logs in stdout * backwards compat for .data/logs/chat.log * le fix * prefer const * Use `logging.yaml.example` * update docs for logging.yaml.example * Bump minor version to 5.2.0
1 parent 26b8595 commit d570e77

23 files changed

+174
-126
lines changed

Diff for: .env.example

+2
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ DISCORD_AUDIT_LOG_WEBHOOK=
6363
# for debugging & development purposes.
6464
AUTOMATICALLY_REPORT_PROBLEMS=False
6565

66+
LOG_WITH_COLORS=False
67+
6668
# XXX: Uncomment this if you have downloaded the database from maxmind.
6769
# Change the path to the .mmdb file you downloaded, uncomment here and in docker-compose.yml
6870
# You can download the database here: https://dev.maxmind.com/geoip/geolite2-free-geolocation-data

Diff for: .github/docs/wiki/Setting-up.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ sudo apt install -y docker docker-compose
1717
## configuring bancho.py
1818

1919
all configuration for the osu! server (bancho.py) itself can be done from the
20-
`.env` file. we provide an example `.env.example` file which you can use as a base.
20+
`.env` and `logging.yaml` files. we will provide example files for each, which
21+
you can use as a base and modify as you'd like.
2122

2223
```sh
2324
# create a configuration file from the sample provided
2425
cp .env.example .env
2526

27+
# create a logging configuration file from the sample provided
28+
cp logging.yaml.example logging.yaml
29+
2630
# configure the application to your needs
2731
# this is required to move onto the next steps
2832
nano .env
33+
34+
# you can additionally configure the logging if you'd like,
35+
# but the default should work fine for most users.
36+
nano logging.yaml
2937
```
3038

3139
## configuring a reverse proxy (we'll use nginx)

Diff for: .github/docs/wiki/locale/de-DE/Setting-up-de-DE.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ sudo apt install -y docker docker-compose
1717
## configuring bancho.py
1818

1919
all configuration for the osu! server (bancho.py) itself can be done from the
20-
`.env` file. we provide an example `.env.example` file which you can use as a base.
20+
`.env` and `logging.yaml` files. we will provide example files for each, which
21+
you can use as a base and modify as you'd like.
2122

2223
```sh
2324
# create a configuration file from the sample provided
2425
cp .env.example .env
2526

27+
# create a logging configuration file from the sample provided
28+
cp logging.yaml.example logging.yaml
29+
2630
# configure the application to your needs
2731
# this is required to move onto the next steps
2832
nano .env
33+
34+
# you can additionally configure the logging if you'd like,
35+
# but the default should work fine for most users.
36+
nano logging.yaml
2937
```
3038

3139
## configuring a reverse proxy (we'll use nginx)

Diff for: .github/docs/wiki/locale/zh-CN/Setting-up-zh-CN.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ sudo apt install -y docker docker-compose
1717
## configuring bancho.py
1818

1919
all configuration for the osu! server (bancho.py) itself can be done from the
20-
`.env` file. we provide an example `.env.example` file which you can use as a base.
20+
`.env` and `logging.yaml` files. we will provide example files for each, which
21+
you can use as a base and modify as you'd like.
2122

2223
```sh
2324
# create a configuration file from the sample provided
2425
cp .env.example .env
2526

27+
# create a logging configuration file from the sample provided
28+
cp logging.yaml.example logging.yaml
29+
2630
# configure the application to your needs
2731
# this is required to move onto the next steps
2832
nano .env
33+
34+
# you can additionally configure the logging if you'd like,
35+
# but the default should work fine for most users.
36+
nano logging.yaml
2937
```
3038

3139
## configuring a reverse proxy (we'll use nginx)

Diff for: .github/workflows/test.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ env:
1818
APP_HOST: "0.0.0.0"
1919
APP_PORT: "10000"
2020
AUTOMATICALLY_REPORT_PROBLEMS: "False"
21+
LOG_WITH_COLORS: "False"
2122
COMMAND_PREFIX: "!"
2223
DATA_DIRECTORY: "not relevant"
2324
DB_HOST: "mysql"

Diff for: .gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ tools/cf_records.txt
1717
/.redis-data/
1818
poetry.toml
1919
.coverage
20+
logging.yaml
21+
logs.log

Diff for: app/adapters/database.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sqlalchemy.sql.expression import ClauseElement
1111

1212
from app import settings
13+
from app.logging import log
1314

1415

1516
class MySQLDialect(MySQLDialect_mysqldb):
@@ -38,8 +39,6 @@ def _compile(self, clause_element: ClauseElement) -> tuple[str, MySQLParams]:
3839
dialect=DIALECT,
3940
compile_kwargs={"render_postcompile": True},
4041
)
41-
if settings.DEBUG:
42-
print(str(compiled), compiled.params)
4342
return str(compiled), compiled.params
4443

4544
async def fetch_one(
@@ -51,6 +50,12 @@ async def fetch_one(
5150
query, params = self._compile(query)
5251

5352
row = await self._database.fetch_one(query, params)
53+
if settings.DEBUG:
54+
log(
55+
f"Executed SQL query: {query} {params}",
56+
extra={"query": query, "params": params},
57+
)
58+
5459
return dict(row._mapping) if row is not None else None
5560

5661
async def fetch_all(

Diff for: app/api/domains/cho.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import logging
67
import re
78
import struct
89
import time
@@ -13,6 +14,7 @@
1314
from pathlib import Path
1415
from typing import Literal
1516
from typing import TypedDict
17+
from zoneinfo import ZoneInfo
1618

1719
import bcrypt
1820
import databases.core
@@ -37,6 +39,7 @@
3739
from app.constants.privileges import ClientPrivileges
3840
from app.constants.privileges import Privileges
3941
from app.logging import Ansi
42+
from app.logging import get_timestamp
4043
from app.logging import log
4144
from app.logging import magnitude_fmt_time
4245
from app.objects.beatmap import Beatmap
@@ -69,6 +72,7 @@
6972
OSU_API_V2_CHANGELOG_URL = "https://osu.ppy.sh/api/v2/changelog"
7073

7174
BEATMAPS_PATH = Path.cwd() / ".data/osu"
75+
DISK_CHAT_LOG_FILE = ".data/logs/chat.log"
7276

7377
BASE_DOMAIN = app.settings.DOMAIN
7478

@@ -424,7 +428,13 @@ async def handle(self, player: Player) -> None:
424428
t_chan.send(msg, sender=player)
425429

426430
player.update_latest_activity_soon()
427-
log(f"{player} @ {t_chan}: {msg}", Ansi.LCYAN, file=".data/logs/chat.log")
431+
432+
log(f"{player} @ {t_chan}: {msg}", Ansi.LCYAN)
433+
434+
with open(DISK_CHAT_LOG_FILE, "a+") as f:
435+
f.write(
436+
f"[{get_timestamp(full=True, tz=ZoneInfo('GMT'))}] {player} @ {t_chan}: {msg}\n",
437+
)
428438

429439

430440
@register(ClientPackets.LOGOUT, restricted=True)
@@ -1298,7 +1308,12 @@ async def handle(self, player: Player) -> None:
12981308
player.send(resp_msg, sender=target)
12991309

13001310
player.update_latest_activity_soon()
1301-
log(f"{player} @ {target}: {msg}", Ansi.LCYAN, file=".data/logs/chat.log")
1311+
1312+
log(f"{player} @ {target}: {msg}", Ansi.LCYAN)
1313+
with open(DISK_CHAT_LOG_FILE, "a+") as f:
1314+
f.write(
1315+
f"[{get_timestamp(full=True, tz=ZoneInfo('GMT'))}] {player} @ {target}: {msg}\n",
1316+
)
13021317

13031318

13041319
@register(ClientPackets.PART_LOBBY)

Diff for: app/api/domains/osu.py

-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
from app.constants.privileges import Privileges
5252
from app.logging import Ansi
5353
from app.logging import log
54-
from app.logging import printc
5554
from app.objects import models
5655
from app.objects.beatmap import Beatmap
5756
from app.objects.beatmap import RankedStatus

Diff for: app/api/init_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ async def lifespan(asgi_app: BanchoAPI) -> AsyncIterator[None]:
7979
if app.utils.is_running_as_admin():
8080
log(
8181
"Running the server with root privileges is not recommended.",
82-
Ansi.LRED,
82+
Ansi.LYELLOW,
8383
)
8484

8585
await app.state.services.database.connect()

Diff for: app/api/middlewares.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from app.logging import Ansi
1111
from app.logging import log
1212
from app.logging import magnitude_fmt_time
13-
from app.logging import printc
1413

1514

1615
class MetricsMiddleware(BaseHTTPMiddleware):
@@ -25,16 +24,14 @@ async def dispatch(
2524

2625
time_elapsed = end_time - start_time
2726

28-
col = (
29-
Ansi.LGREEN
30-
if 200 <= response.status_code < 300
31-
else Ansi.LYELLOW if 300 <= response.status_code < 400 else Ansi.LRED
32-
)
27+
col = Ansi.LGREEN if response.status_code < 400 else Ansi.LRED
3328

3429
url = f"{request.headers['host']}{request['path']}"
3530

36-
log(f"[{request.method}] {response.status_code} {url}", col, end=" | ")
37-
printc(f"Request took: {magnitude_fmt_time(time_elapsed)}", Ansi.LBLUE)
31+
log(
32+
f"[{request.method}] {response.status_code} {url}{Ansi.RESET!r} | {Ansi.LBLUE!r}Request took: {magnitude_fmt_time(time_elapsed)}",
33+
col,
34+
)
3835

3936
response.headers["process-time"] = str(round(time_elapsed) / 1e6)
4037
return response

Diff for: app/logging.py

+36-80
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
from __future__ import annotations
22

3-
import colorsys
43
import datetime
4+
import logging.config
5+
import re
6+
from collections.abc import Mapping
57
from enum import IntEnum
68
from zoneinfo import ZoneInfo
79

10+
import yaml
11+
12+
from app import settings
13+
14+
15+
def configure_logging() -> None:
16+
with open("logging.yaml") as f:
17+
config = yaml.safe_load(f.read())
18+
logging.config.dictConfig(config)
19+
820

921
class Ansi(IntEnum):
1022
# Default colours
@@ -33,105 +45,49 @@ def __repr__(self) -> str:
3345
return f"\x1b[{self.value}m"
3446

3547

36-
class RGB:
37-
def __init__(self, *args: int) -> None:
38-
largs = len(args)
39-
40-
if largs == 3:
41-
# r, g, b passed.
42-
self.r, self.g, self.b = args
43-
elif largs == 1:
44-
# passed as single argument
45-
rgb = args[0]
46-
self.b = rgb & 0xFF
47-
self.g = (rgb >> 8) & 0xFF
48-
self.r = (rgb >> 16) & 0xFF
49-
else:
50-
raise ValueError("Incorrect params for RGB.")
51-
52-
def __repr__(self) -> str:
53-
return f"\x1b[38;2;{self.r};{self.g};{self.b}m"
54-
55-
56-
class _Rainbow: ...
57-
58-
59-
Rainbow = _Rainbow()
60-
61-
Colour_Types = Ansi | RGB | _Rainbow
62-
63-
6448
def get_timestamp(full: bool = False, tz: ZoneInfo | None = None) -> str:
6549
fmt = "%d/%m/%Y %I:%M:%S%p" if full else "%I:%M:%S%p"
6650
return f"{datetime.datetime.now(tz=tz):{fmt}}"
6751

6852

69-
# TODO: better solution than this; this at least requires the
70-
# iana/tzinfo database to be installed, meaning it's limited.
71-
_log_tz = ZoneInfo("GMT") # default
53+
ANSI_ESCAPE_REGEX = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
7254

7355

74-
def set_timezone(tz: ZoneInfo) -> None:
75-
global _log_tz
76-
_log_tz = tz
56+
def escape_ansi(line: str) -> str:
57+
return ANSI_ESCAPE_REGEX.sub("", line)
7758

7859

79-
def printc(msg: str, col: Colour_Types, end: str = "\n") -> None:
80-
"""Print a string, in a specified ansi colour."""
81-
print(f"{col!r}{msg}{Ansi.RESET!r}", end=end)
60+
ROOT_LOGGER = logging.getLogger()
8261

8362

8463
def log(
8564
msg: str,
86-
col: Colour_Types | None = None,
87-
file: str | None = None,
88-
end: str = "\n",
65+
start_color: Ansi | None = None,
66+
extra: Mapping[str, object] | None = None,
8967
) -> None:
9068
"""\
91-
Print a string, in a specified ansi colour with timestamp.
92-
93-
Allows for the functionality to write to a file as
94-
well by passing the filepath with the `file` parameter.
69+
A thin wrapper around the stdlib logging module to handle mostly
70+
backwards-compatibility for colours during our migration to the
71+
standard library logging module.
9572
"""
9673

97-
ts_short = get_timestamp(full=False, tz=_log_tz)
98-
99-
if col:
100-
if col is Rainbow:
101-
print(f"{Ansi.GRAY!r}[{ts_short}] {_fmt_rainbow(msg, 2/3)}", end=end)
102-
print(f"{Ansi.GRAY!r}[{ts_short}] {_fmt_rainbow(msg, 2/3)}", end=end)
103-
else:
104-
# normal colour
105-
print(f"{Ansi.GRAY!r}[{ts_short}] {col!r}{msg}{Ansi.RESET!r}", end=end)
74+
# TODO: decouple colors from the base logging function; move it to
75+
# be a formatter-specific concern such that we can log without color.
76+
if start_color is Ansi.LYELLOW:
77+
log_level = logging.WARNING
78+
elif start_color is Ansi.LRED:
79+
log_level = logging.ERROR
10680
else:
107-
print(f"{Ansi.GRAY!r}[{ts_short}]{Ansi.RESET!r} {msg}", end=end)
108-
109-
if file:
110-
# log simple ascii output to file.
111-
with open(file, "a+") as f:
112-
f.write(f"[{get_timestamp(full=True, tz=_log_tz)}] {msg}\n")
113-
114-
115-
def rainbow_color_stops(
116-
n: int = 10,
117-
lum: float = 0.5,
118-
end: float = 2 / 3,
119-
) -> list[tuple[float, float, float]]:
120-
return [
121-
(r * 255, g * 255, b * 255)
122-
for r, g, b in [
123-
colorsys.hls_to_rgb(end * i / (n - 1), lum, 1) for i in range(n)
124-
]
125-
]
126-
127-
128-
def _fmt_rainbow(msg: str, end: float = 2 / 3) -> str:
129-
cols = [RGB(*map(int, rgb)) for rgb in rainbow_color_stops(n=len(msg), end=end)]
130-
return "".join([f"{cols[i]!r}{c}" for i, c in enumerate(msg)]) + repr(Ansi.RESET)
81+
log_level = logging.INFO
13182

83+
if settings.LOG_WITH_COLORS:
84+
color_prefix = f"{start_color!r}" if start_color is not None else ""
85+
color_suffix = f"{Ansi.RESET!r}" if start_color is not None else ""
86+
else:
87+
msg = escape_ansi(msg)
88+
color_prefix = color_suffix = ""
13289

133-
def print_rainbow(msg: str, rainbow_end: float = 2 / 3, end: str = "\n") -> None:
134-
print(_fmt_rainbow(msg, rainbow_end), end=end)
90+
ROOT_LOGGER.log(log_level, f"{color_prefix}{msg}{color_suffix}", extra=extra)
13591

13692

13793
TIME_ORDER_SUFFIXES = ["nsec", "μsec", "msec", "sec"]

Diff for: app/objects/player.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ def logout(self) -> None:
403403

404404
app.state.sessions.players.enqueue(app.packets.logout(self.id))
405405

406-
log(f"{self} logged out.", Ansi.LYELLOW)
406+
log(f"{self} logged out.")
407407

408408
async def update_privs(self, new: Privileges) -> None:
409409
"""Update `self`'s privileges to `new`."""

0 commit comments

Comments
 (0)