Skip to content

Commit 6f82883

Browse files
authored
Merge pull request #209 from grillazz/198-add-simple-caching
add structure file logging with log files rotating
2 parents 6c54aee + 63859e8 commit 6f82883

File tree

9 files changed

+65
-33
lines changed

9 files changed

+65
-33
lines changed

app/api/health.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import logging
21
from typing import Annotated
32

43
from fastapi import APIRouter, Depends, Query, Request, status
@@ -34,7 +33,7 @@ async def redis_check(request: Request):
3433
try:
3534
redis_info = await redis_client.info()
3635
except Exception as e:
37-
logging.error(f"Redis error: {e}")
36+
await logger.aerror(f"Redis error: {e}")
3837
return redis_info
3938

4039

@@ -88,7 +87,7 @@ async def smtp_check(
8887
"subject": subject,
8988
}
9089

91-
logger.info("Sending email with data: %s", email_data)
90+
await logger.ainfo("Sending email.", email_data=email_data)
9291

9392
await run_in_threadpool(
9493
smtp.send_email,

app/api/stuff.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ async def create_multi_stuff(
2121
db_session.add_all(stuff_instances)
2222
await db_session.commit()
2323
except SQLAlchemyError as ex:
24-
logger.error(f"Error inserting instances of Stuff: {repr(ex)}")
24+
await logger.aerror(f"Error inserting instances of Stuff: {repr(ex)}")
2525
raise HTTPException(
2626
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
2727
) from ex
2828
else:
29-
logger.info(
30-
f"{len(stuff_instances)} instances of Stuff inserted into database."
29+
await logger.ainfo(
30+
f"{len(stuff_instances)} Stuff instances inserted into the database."
3131
)
3232
return True
3333

app/api/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
async def create_user(
1919
payload: UserSchema, request: Request, db_session: AsyncSession = Depends(get_db)
2020
):
21-
logger.info(f"Creating user: {payload}")
21+
await logger.ainfo(f"Creating user: {payload}")
2222
_user: User = User(**payload.model_dump())
2323
await _user.save(db_session)
2424

app/database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ async def get_db() -> AsyncGenerator:
2929
try:
3030
yield session
3131
except Exception as e:
32-
logger.error(f"Error getting database session: {e}")
32+
await logger.aerror(f"Error getting database session: {e}")
3333
raise

app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async def lifespan(app: FastAPI):
3030
min_size=5,
3131
max_size=20,
3232
)
33-
logger.info("Postgres pool created", idle_size=app.postgres_pool.get_idle_size())
33+
await logger.ainfo("Postgres pool created", idle_size=app.postgres_pool.get_idle_size())
3434
yield
3535
finally:
3636
await app.redis.close()

app/models/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async def save(self, db_session: AsyncSession):
3030
db_session.add(self)
3131
return await db_session.commit()
3232
except SQLAlchemyError as ex:
33-
logger.error(f"Error inserting instance of {self}: {repr(ex)}")
33+
await logger.aerror(f"Error inserting instance of {self}: {repr(ex)}")
3434
raise HTTPException(
3535
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=repr(ex)
3636
) from ex

app/services/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def __call__(self, request: Request):
4040
raise HTTPException(
4141
status_code=403, detail="Invalid token or expired token."
4242
)
43-
logger.info(f"Token verified: {credentials.credentials}")
43+
await logger.ainfo(f"Token verified: {credentials.credentials}")
4444
return credentials.credentials
4545

4646

app/services/scheduler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
async def tick():
1616
async with AsyncSessionFactory() as session:
1717
stmt = text("select 1;")
18-
logger.info(f">>>> Be or not to be...{datetime.now()}")
18+
await logger.ainfo(f">>>> Be or not to be...{datetime.now()}")
1919
result = await session.execute(stmt)
20-
logger.info(f">>>> Result: {result.scalar()}")
20+
await logger.ainfo(f">>>> Result: {result.scalar()}")
2121
return True
2222

2323

app/utils/logging.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,63 @@
1111
from app.utils.singleton import SingletonMetaNoArgs
1212

1313

14-
# TODO: merge this wrapper with the one in structlog under one hood of AppLogger
15-
class BytesToTextIOWrapper:
16-
def __init__(self, handler, encoding="utf-8"):
14+
class RotatingBytesLogger:
15+
"""Logger that respects RotatingFileHandler's rotation capabilities."""
16+
17+
def __init__(self, handler):
1718
self.handler = handler
18-
self.encoding = encoding
1919

20-
def write(self, b):
21-
if isinstance(b, bytes):
22-
self.handler.stream.write(b.decode(self.encoding))
23-
else:
24-
self.handler.stream.write(b)
25-
self.handler.flush()
20+
def msg(self, message):
21+
"""Process a message and pass it through the handler's emit method."""
22+
if isinstance(message, bytes):
23+
message = message.decode("utf-8")
24+
25+
# Create a log record that will trigger rotation checks
26+
record = logging.LogRecord(
27+
name="structlog",
28+
level=logging.INFO,
29+
pathname="",
30+
lineno=0,
31+
msg=message.rstrip("\n"),
32+
args=(),
33+
exc_info=None
34+
)
35+
36+
# Check if rotation is needed before emitting
37+
if self.handler.shouldRollover(record):
38+
self.handler.doRollover()
39+
40+
# Emit the record through the handler
41+
self.handler.emit(record)
42+
43+
# Required methods to make it compatible with structlog
44+
def debug(self, message):
45+
self.msg(message)
46+
47+
def info(self, message):
48+
self.msg(message)
2649

27-
def flush(self):
28-
self.handler.flush()
50+
def warning(self, message):
51+
self.msg(message)
52+
53+
def error(self, message):
54+
self.msg(message)
55+
56+
def critical(self, message):
57+
self.msg(message)
58+
59+
60+
class RotatingBytesLoggerFactory:
61+
"""Factory that creates loggers that respect file rotation."""
62+
63+
def __init__(self, handler):
64+
self.handler = handler
2965

30-
def close(self):
31-
self.handler.close()
66+
def __call__(self, *args, **kwargs):
67+
return RotatingBytesLogger(self.handler)
3268

3369

34-
@define(slots=True)
70+
@define
3571
class AppStructLogger(metaclass=SingletonMetaNoArgs):
3672
_logger: structlog.BoundLogger = field(init=False)
3773

@@ -40,8 +76,7 @@ def __attrs_post_init__(self):
4076
_log_path = Path(f"{_log_date}_{os.getpid()}.log")
4177
_handler = RotatingFileHandler(
4278
filename=_log_path,
43-
mode="a",
44-
maxBytes=10 * 1024 * 1024,
79+
maxBytes=10 * 1024 * 1024, # 10MB
4580
backupCount=5,
4681
encoding="utf-8"
4782
)
@@ -55,9 +90,7 @@ def __attrs_post_init__(self):
5590
structlog.processors.TimeStamper(fmt="iso", utc=True),
5691
structlog.processors.JSONRenderer(serializer=orjson.dumps),
5792
],
58-
logger_factory=structlog.BytesLoggerFactory(
59-
file=BytesToTextIOWrapper(_handler)
60-
)
93+
logger_factory=RotatingBytesLoggerFactory(_handler)
6194
)
6295
self._logger = structlog.get_logger()
6396

0 commit comments

Comments
 (0)