Skip to content

Commit 73278fc

Browse files
authored
Merge branch 'main' into main
2 parents 2dfa314 + cdcc9e1 commit 73278fc

41 files changed

Lines changed: 1490 additions & 315 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/app/database/images.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from app.config.settings import (
77
DATABASE_PATH,
88
)
9+
from app.logging.setup_logging import get_logger
10+
11+
# Initialize logger
12+
logger = get_logger(__name__)
913

1014
# Type definitions
1115
ImageId = str
@@ -108,7 +112,7 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool:
108112
conn.commit()
109113
return True
110114
except Exception as e:
111-
print(f"Error inserting image records: {e}")
115+
logger.error(f"Error inserting image records: {e}")
112116
conn.rollback()
113117
return False
114118
finally:
@@ -189,7 +193,7 @@ def db_get_all_images() -> List[dict]:
189193
return images
190194

191195
except Exception as e:
192-
print(f"Error getting all images: {e}")
196+
logger.error(f"Error getting all images: {e}")
193197
return []
194198
finally:
195199
conn.close()
@@ -264,7 +268,7 @@ def db_update_image_tagged_status(image_id: ImageId, is_tagged: bool = True) ->
264268
conn.commit()
265269
return cursor.rowcount > 0
266270
except Exception as e:
267-
print(f"Error updating image tagged status: {e}")
271+
logger.error(f"Error updating image tagged status: {e}")
268272
conn.rollback()
269273
return False
270274
finally:
@@ -298,7 +302,7 @@ def db_insert_image_classes_batch(image_class_pairs: List[ImageClassPair]) -> bo
298302
conn.commit()
299303
return True
300304
except Exception as e:
301-
print(f"Error inserting image classes: {e}")
305+
logger.error(f"Error inserting image classes: {e}")
302306
conn.rollback()
303307
return False
304308
finally:
@@ -336,7 +340,7 @@ def db_get_images_by_folder_ids(
336340
)
337341
return cursor.fetchall()
338342
except Exception as e:
339-
print(f"Error getting images by folder IDs: {e}")
343+
logger.error(f"Error getting images by folder IDs: {e}")
340344
return []
341345
finally:
342346
conn.close()
@@ -367,10 +371,10 @@ def db_delete_images_by_ids(image_ids: List[ImageId]) -> bool:
367371
image_ids,
368372
)
369373
conn.commit()
370-
print(f"Deleted {cursor.rowcount} obsolete image(s) from database")
374+
logger.info(f"Deleted {cursor.rowcount} obsolete image(s) from database")
371375
return True
372376
except Exception as e:
373-
print(f"Error deleting images: {e}")
377+
logger.error(f"Error deleting images: {e}")
374378
conn.rollback()
375379
return False
376380
finally:

backend/app/logging/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
__init__.py for the backend.app.logging package.
3+
4+
This file allows the package to be imported and initializes logging.
5+
"""
6+
7+
from .setup_logging import get_logger, configure_uvicorn_logging, setup_logging
8+
9+
__all__ = ["get_logger", "configure_uvicorn_logging", "setup_logging"]
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
"""
2+
Core logging module for the PictoPy project.
3+
4+
This module provides centralized logging functionality for all components
5+
of the PictoPy project, including color coding and consistent formatting.
6+
"""
7+
8+
import os
9+
import json
10+
import logging
11+
import sys
12+
from pathlib import Path
13+
from typing import Optional, Dict, Any
14+
15+
16+
class ColorFormatter(logging.Formatter):
17+
"""
18+
Custom formatter that adds color to log messages based on their level.
19+
"""
20+
21+
# ANSI color codes
22+
COLORS = {
23+
"black": "\033[30m",
24+
"red": "\033[31m",
25+
"green": "\033[32m",
26+
"yellow": "\033[33m",
27+
"blue": "\033[34m",
28+
"magenta": "\033[35m",
29+
"cyan": "\033[36m",
30+
"white": "\033[37m",
31+
"bg_black": "\033[40m",
32+
"bg_red": "\033[41m",
33+
"bg_green": "\033[42m",
34+
"bg_yellow": "\033[43m",
35+
"bg_blue": "\033[44m",
36+
"bg_magenta": "\033[45m",
37+
"bg_cyan": "\033[46m",
38+
"bg_white": "\033[47m",
39+
"reset": "\033[0m",
40+
}
41+
42+
def __init__(
43+
self,
44+
fmt: str,
45+
component_config: Dict[str, Any],
46+
level_colors: Dict[str, str],
47+
use_colors: bool = True,
48+
):
49+
"""
50+
Initialize the formatter with the given format string and color settings.
51+
52+
Args:
53+
fmt: The format string to use
54+
component_config: Configuration for the component (prefix and color)
55+
level_colors: Dictionary mapping log levels to colors
56+
use_colors: Whether to use colors in log output
57+
"""
58+
super().__init__(fmt)
59+
self.component_config = component_config
60+
self.level_colors = level_colors
61+
self.use_colors = use_colors
62+
63+
def format(self, record: logging.LogRecord) -> str:
64+
"""Format the log record with colors and component prefix."""
65+
# Add component information to the record
66+
component_prefix = self.component_config.get("prefix", "")
67+
record.component = component_prefix
68+
69+
# Format the message
70+
formatted_message = super().format(record)
71+
72+
if not self.use_colors:
73+
return formatted_message
74+
75+
# Add color to the component prefix
76+
component_color = self.component_config.get("color", "")
77+
if component_color and component_color in self.COLORS:
78+
component_start = formatted_message.find(f"[{component_prefix}]")
79+
if component_start >= 0:
80+
component_end = component_start + len(f"[{component_prefix}]")
81+
formatted_message = (
82+
formatted_message[:component_start]
83+
+ self.COLORS[component_color]
84+
+ formatted_message[component_start:component_end]
85+
+ self.COLORS["reset"]
86+
+ formatted_message[component_end:]
87+
)
88+
89+
# Add color to the log level
90+
level_color = self.level_colors.get(record.levelname, "")
91+
if level_color:
92+
# Handle comma-separated color specs like "red,bg_white"
93+
color_codes = ""
94+
for color in level_color.split(","):
95+
if color in self.COLORS:
96+
color_codes += self.COLORS[color]
97+
98+
if color_codes:
99+
level_start = formatted_message.find(f" {record.levelname} ")
100+
if level_start >= 0:
101+
level_end = level_start + len(f" {record.levelname} ")
102+
formatted_message = (
103+
formatted_message[:level_start]
104+
+ color_codes
105+
+ formatted_message[level_start:level_end]
106+
+ self.COLORS["reset"]
107+
+ formatted_message[level_end:]
108+
)
109+
110+
return formatted_message
111+
112+
113+
def load_config() -> Dict[str, Any]:
114+
"""
115+
Load the logging configuration from the JSON file.
116+
117+
Returns:
118+
Dict containing the logging configuration
119+
"""
120+
config_path = (
121+
Path(__file__).parent.parent.parent.parent
122+
/ "utils"
123+
/ "logging"
124+
/ "logging_config.json"
125+
)
126+
try:
127+
with open(config_path, "r") as f:
128+
return json.load(f)
129+
except (FileNotFoundError, json.JSONDecodeError) as e:
130+
print(f"Error loading logging configuration: {e}", file=sys.stderr)
131+
return {}
132+
133+
134+
def setup_logging(component_name: str, environment: Optional[str] = None) -> None:
135+
"""
136+
Set up logging for the given component.
137+
138+
Args:
139+
component_name: The name of the component (e.g., "backend", "sync-microservice")
140+
environment: The environment to use (e.g., "development", "production")
141+
If None, uses the environment specified in the config or "development"
142+
"""
143+
config = load_config()
144+
if not config:
145+
print(
146+
"No logging configuration found. Using default settings.", file=sys.stderr
147+
)
148+
return
149+
150+
# Get environment settings
151+
if not environment:
152+
environment = os.environ.get(
153+
"ENV", config.get("default_environment", "development")
154+
)
155+
156+
env_settings = config.get("environments", {}).get(environment, {})
157+
log_level = getattr(logging, env_settings.get("level", "INFO"), logging.INFO)
158+
use_colors = env_settings.get("colored_output", True)
159+
console_logging = env_settings.get("console_logging", True)
160+
161+
# Get component configuration
162+
component_config = config.get("components", {}).get(
163+
component_name, {"prefix": component_name.upper(), "color": "white"}
164+
)
165+
166+
# Configure root logger
167+
root_logger = logging.getLogger()
168+
root_logger.setLevel(log_level)
169+
170+
# Clear existing handlers
171+
for handler in root_logger.handlers[:]:
172+
root_logger.removeHandler(handler)
173+
174+
# Configure specific loggers if defined in environment settings
175+
if "loggers" in env_settings:
176+
for logger_name, logger_config in env_settings["loggers"].items():
177+
logger = logging.getLogger(logger_name)
178+
if "level" in logger_config:
179+
logger.setLevel(getattr(logging, logger_config["level"], log_level))
180+
181+
# Set up console handler
182+
if console_logging:
183+
console_handler = logging.StreamHandler(sys.stdout)
184+
console_handler.setLevel(log_level)
185+
186+
# Create formatter with component and color information
187+
fmt = (
188+
config.get("formatters", {})
189+
.get("default", {})
190+
.get("format", "[%(component)s] | %(levelname)s | %(message)s")
191+
)
192+
formatter = ColorFormatter(
193+
fmt, component_config, config.get("colors", {}), use_colors
194+
)
195+
console_handler.setFormatter(formatter)
196+
root_logger.addHandler(console_handler)
197+
198+
199+
def get_logger(name: str) -> logging.Logger:
200+
"""
201+
Get a logger with the given name.
202+
203+
Args:
204+
name: Name of the logger, typically the module name
205+
206+
Returns:
207+
Logger instance
208+
"""
209+
return logging.getLogger(name)
210+
211+
212+
class InterceptHandler(logging.Handler):
213+
"""
214+
Handler to intercept logs from other loggers (like Uvicorn) and redirect them
215+
through our custom logger.
216+
217+
This implementation is based on Loguru's approach and routes logs directly to
218+
the root logger.
219+
"""
220+
221+
def __init__(self, component_name: str):
222+
"""
223+
Initialize the InterceptHandler.
224+
225+
Args:
226+
component_name: The name of the component (e.g., "backend")
227+
"""
228+
super().__init__()
229+
self.component_name = component_name
230+
231+
def emit(self, record: logging.LogRecord) -> None:
232+
"""
233+
Process a log record by forwarding it through our custom logger.
234+
235+
Args:
236+
record: The log record to process
237+
"""
238+
# Get the appropriate module name
239+
module_name = record.name
240+
if "." in module_name:
241+
module_name = module_name.split(".")[-1]
242+
243+
# Create a message that includes the original module in the format
244+
msg = record.getMessage()
245+
246+
# Find the appropriate logger
247+
logger = get_logger(module_name)
248+
249+
# Log the message with our custom formatting
250+
logger.log(record.levelno, f"[uvicorn] {msg}")
251+
252+
253+
def configure_uvicorn_logging(component_name: str) -> None:
254+
"""
255+
Configure Uvicorn logging to match our format.
256+
257+
Args:
258+
component_name: The name of the component (e.g., "backend")
259+
"""
260+
import logging.config
261+
262+
# Create an intercept handler with our component name
263+
intercept_handler = InterceptHandler(component_name)
264+
265+
# Make sure the handler uses our ColorFormatter
266+
config = load_config()
267+
component_config = config.get("components", {}).get(
268+
component_name, {"prefix": component_name.upper(), "color": "white"}
269+
)
270+
level_colors = config.get("colors", {})
271+
env_settings = config.get("environments", {}).get(
272+
os.environ.get("ENV", config.get("default_environment", "development")), {}
273+
)
274+
use_colors = env_settings.get("colored_output", True)
275+
276+
fmt = "[%(component)s] | %(module)s | %(levelname)s | %(message)s"
277+
formatter = ColorFormatter(fmt, component_config, level_colors, use_colors)
278+
intercept_handler.setFormatter(formatter)
279+
280+
# Configure Uvicorn loggers to use our handler
281+
for logger_name in ["uvicorn", "uvicorn.error", "uvicorn.access"]:
282+
uvicorn_logger = logging.getLogger(logger_name)
283+
uvicorn_logger.handlers = [] # Clear existing handlers
284+
uvicorn_logger.propagate = False # Don't propagate to root
285+
uvicorn_logger.setLevel(logging.INFO) # Ensure log level is at least INFO
286+
uvicorn_logger.addHandler(intercept_handler)
287+
288+
# Also configure asyncio logger similarly
289+
asyncio_logger = logging.getLogger("asyncio")
290+
asyncio_logger.handlers = []
291+
asyncio_logger.propagate = False
292+
asyncio_logger.addHandler(intercept_handler)

0 commit comments

Comments
 (0)