gpclog 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpclog/__init__.py +91 -0
- gpclog/config.py +55 -0
- gpclog/logger.py +149 -0
- gpclog/manager.py +201 -0
- gpclog/utils.py +60 -0
- gpclog-0.2.0.dist-info/METADATA +114 -0
- gpclog-0.2.0.dist-info/RECORD +9 -0
- gpclog-0.2.0.dist-info/WHEEL +4 -0
- gpclog-0.2.0.dist-info/licenses/LICENSE +21 -0
gpclog/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""gpclog - General Purpose Category Logger.
|
|
2
|
+
|
|
3
|
+
用于在复杂Python项目里管理分类日志。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from gpconfig import GPConfigFolder, GPConfigManager
|
|
9
|
+
|
|
10
|
+
from gpclog.config import GPCLoggerConfig
|
|
11
|
+
from gpclog.logger import GPCLogger
|
|
12
|
+
from gpclog.manager import GPCLoggerManager
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
|
|
16
|
+
# Auto-register config and configurable classes with GPConfigManager
|
|
17
|
+
GPConfigManager.register_config_class(GPCLoggerConfig)
|
|
18
|
+
GPConfigManager.register_configurable_class(GPCLogger)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"get_logger",
|
|
22
|
+
"set_config_folder",
|
|
23
|
+
"reset",
|
|
24
|
+
"GPCLogger",
|
|
25
|
+
"GPCLoggerConfig",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
# Singleton manager instance
|
|
29
|
+
_manager: GPCLoggerManager = GPCLoggerManager()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_logger(logger_name: str, sn: Optional[int] = None) -> GPCLogger:
|
|
33
|
+
"""Get or create a logger by name.
|
|
34
|
+
|
|
35
|
+
This is the primary interface for obtaining loggers. Each unique
|
|
36
|
+
logger_name creates a separate logger with its own configuration
|
|
37
|
+
and output file.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
logger_name: Unique name for the logger. This name is used
|
|
41
|
+
for configuration lookup and (without sn) for the log file name.
|
|
42
|
+
sn: Optional serial number for multiprocess isolation. When provided,
|
|
43
|
+
the logger is named "{logger_name}-{sn}" and writes to
|
|
44
|
+
"{logger_name}-{sn}.log", but still uses the configuration
|
|
45
|
+
associated with logger_name.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
GPCLogger instance for the given name.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> db_logger = gpclog.get_logger("database")
|
|
52
|
+
>>> db_logger.info("Connected to database")
|
|
53
|
+
>>>
|
|
54
|
+
>>> # In multiprocess scenario:
|
|
55
|
+
>>> worker_logger = gpclog.get_logger("worker", sn=process_id)
|
|
56
|
+
>>> worker_logger.info("Process started")
|
|
57
|
+
"""
|
|
58
|
+
return _manager.get_logger(logger_name, sn)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def set_config_folder(cfg_folder: GPConfigFolder) -> None:
|
|
62
|
+
"""Set the configuration folder for logger configurations.
|
|
63
|
+
|
|
64
|
+
After calling this function, logger configurations will be loaded
|
|
65
|
+
from the specified folder. If a logger's configuration is not found,
|
|
66
|
+
the default configuration is used.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
cfg_folder: GPConfigFolder instance containing logger
|
|
70
|
+
configuration files.
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
>>> from gpconfig import GPConfigManager, GPConfigFolder
|
|
74
|
+
>>> manager = GPConfigManager("myapp")
|
|
75
|
+
>>> folder = GPConfigFolder(manager, "logs")
|
|
76
|
+
>>> gpclog.set_config_folder(folder)
|
|
77
|
+
"""
|
|
78
|
+
_manager.set_config_folder(cfg_folder)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def reset() -> None:
|
|
82
|
+
"""Reset the logger manager and clear all caches.
|
|
83
|
+
|
|
84
|
+
This function is primarily intended for testing purposes to ensure
|
|
85
|
+
a clean state between tests. It resets the internal state and clears
|
|
86
|
+
all cached loggers.
|
|
87
|
+
"""
|
|
88
|
+
global _manager
|
|
89
|
+
GPCLoggerManager._instance = None
|
|
90
|
+
GPCLogger.clear_cache()
|
|
91
|
+
_manager = GPCLoggerManager()
|
gpclog/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# src/gpclog/config.py
|
|
2
|
+
"""GPCLoggerConfig - Configuration class for GPCLogger."""
|
|
3
|
+
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
|
|
6
|
+
from gpconfig import GPConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GPCLoggerConfig(GPConfig):
|
|
10
|
+
"""Configuration for GPCLogger.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
cfg_class_name: Class identifier for gpconfig auto-detection.
|
|
14
|
+
configured_class_name: Name of the configurable class (always "GPCLogger").
|
|
15
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
16
|
+
file_format: Format string for file output.
|
|
17
|
+
console_format: Format string for console output.
|
|
18
|
+
output_to_stdout: Whether to output to stdout.
|
|
19
|
+
output_to_stderr: Whether to output to stderr.
|
|
20
|
+
output_to_file: Whether to output to file.
|
|
21
|
+
log_path: Log output path (auto, env, home, or absolute path).
|
|
22
|
+
rotation_enabled: Whether to enable log rotation.
|
|
23
|
+
rotation_size: Rotation size threshold (e.g., "10 MB").
|
|
24
|
+
retention_enabled: Whether to enable log retention/deletion.
|
|
25
|
+
retention_days: Number of days to retain logs.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
cfg_class_name: ClassVar[str] = "GPCLoggerConfig"
|
|
29
|
+
|
|
30
|
+
# Required fields
|
|
31
|
+
configured_class_name: str = "GPCLogger"
|
|
32
|
+
level: str = "INFO"
|
|
33
|
+
|
|
34
|
+
# Optional fields with defaults
|
|
35
|
+
file_format: str = (
|
|
36
|
+
"{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {extra[name]} | {message}"
|
|
37
|
+
)
|
|
38
|
+
console_format: str = (
|
|
39
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|
40
|
+
"<level>{level: <8}</level> | "
|
|
41
|
+
"<cyan>{extra[name]}</cyan> | "
|
|
42
|
+
"<level>{message}</level>"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
output_to_stdout: bool = True
|
|
46
|
+
output_to_stderr: bool = False
|
|
47
|
+
output_to_file: bool = True
|
|
48
|
+
|
|
49
|
+
log_path: str = "auto"
|
|
50
|
+
|
|
51
|
+
rotation_enabled: bool = False
|
|
52
|
+
rotation_size: str = "10 MB"
|
|
53
|
+
|
|
54
|
+
retention_enabled: bool = False
|
|
55
|
+
retention_days: int = 7
|
gpclog/logger.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# src/gpclog/logger.py
|
|
2
|
+
"""GPCLogger - Configurable logger class."""
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, ClassVar
|
|
6
|
+
|
|
7
|
+
from gpconfig import GPConfigurable
|
|
8
|
+
from loguru import logger as loguru_logger
|
|
9
|
+
|
|
10
|
+
from gpclog.config import GPCLoggerConfig
|
|
11
|
+
from gpclog.utils import resolve_log_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GPCLogger(GPConfigurable):
|
|
15
|
+
"""A configurable logger that wraps loguru.
|
|
16
|
+
|
|
17
|
+
This class provides a category-based logger with configuration support
|
|
18
|
+
through GPCLoggerConfig. Each logger instance writes to its own file
|
|
19
|
+
and can be configured independently.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Class-level cache for bound loguru loggers to prevent duplicate handlers
|
|
23
|
+
_bound_loggers: ClassVar[dict[str, Any]] = {}
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def clear_cache(cls) -> None:
|
|
27
|
+
"""Clear the bound loggers cache.
|
|
28
|
+
|
|
29
|
+
This should be called when the singleton manager is reset to ensure
|
|
30
|
+
that cached loggers with stale handlers are not reused.
|
|
31
|
+
"""
|
|
32
|
+
cls._bound_loggers.clear()
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: GPCLoggerConfig) -> None:
|
|
35
|
+
"""Initialize the logger with configuration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config: GPCLoggerConfig instance containing logger settings.
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(config)
|
|
41
|
+
self._name: str = config.name or "default"
|
|
42
|
+
self._logger = self._create_logger(config)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def name(self) -> str:
|
|
46
|
+
"""Get the logger name."""
|
|
47
|
+
return self._name
|
|
48
|
+
|
|
49
|
+
def _create_logger(self, config: GPCLoggerConfig) -> Any:
|
|
50
|
+
"""Create and configure a loguru logger.
|
|
51
|
+
|
|
52
|
+
Uses loguru's filter mechanism to allow multiple GPCLogger instances
|
|
53
|
+
to coexist and write to different files. Each logger binds its name
|
|
54
|
+
to the record's extra dict, and handlers use filters to route
|
|
55
|
+
messages to the appropriate sinks.
|
|
56
|
+
|
|
57
|
+
If a logger with the same name has already been bound, returns the
|
|
58
|
+
cached instance to avoid adding duplicate handlers.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config: Configuration for the logger.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Configured loguru logger instance with bound name.
|
|
65
|
+
"""
|
|
66
|
+
# Check if a logger with this name already exists in cache
|
|
67
|
+
if self._name in self._bound_loggers:
|
|
68
|
+
return self._bound_loggers[self._name]
|
|
69
|
+
|
|
70
|
+
# Create a logger filter function that captures the name by value
|
|
71
|
+
# to avoid closure issues with late binding
|
|
72
|
+
logger_name = self._name
|
|
73
|
+
|
|
74
|
+
def name_filter(record: dict) -> bool:
|
|
75
|
+
"""Filter records by logger name."""
|
|
76
|
+
return record["extra"].get("name") == logger_name
|
|
77
|
+
|
|
78
|
+
# Create a new logger with bound context
|
|
79
|
+
new_logger = loguru_logger.bind(name=self._name)
|
|
80
|
+
|
|
81
|
+
# Add console handler if enabled
|
|
82
|
+
if config.output_to_stdout:
|
|
83
|
+
loguru_logger.add(
|
|
84
|
+
sink=sys.stdout,
|
|
85
|
+
format=config.console_format,
|
|
86
|
+
level=config.level,
|
|
87
|
+
colorize=True,
|
|
88
|
+
filter=name_filter,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Add stderr handler if enabled
|
|
92
|
+
if config.output_to_stderr:
|
|
93
|
+
loguru_logger.add(
|
|
94
|
+
sink=sys.stderr,
|
|
95
|
+
format=config.console_format,
|
|
96
|
+
level=config.level,
|
|
97
|
+
colorize=True,
|
|
98
|
+
filter=name_filter,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Add file handler if enabled
|
|
102
|
+
if config.output_to_file:
|
|
103
|
+
log_dir = resolve_log_path(config.log_path)
|
|
104
|
+
log_file = log_dir / f"{self._name}.log"
|
|
105
|
+
|
|
106
|
+
# Configure rotation
|
|
107
|
+
rotation = None
|
|
108
|
+
if config.rotation_enabled:
|
|
109
|
+
rotation = config.rotation_size
|
|
110
|
+
|
|
111
|
+
# Configure retention
|
|
112
|
+
retention = None
|
|
113
|
+
if config.retention_enabled:
|
|
114
|
+
retention = f"{config.retention_days} days"
|
|
115
|
+
|
|
116
|
+
loguru_logger.add(
|
|
117
|
+
sink=str(log_file),
|
|
118
|
+
format=config.file_format,
|
|
119
|
+
level=config.level,
|
|
120
|
+
rotation=rotation,
|
|
121
|
+
retention=retention,
|
|
122
|
+
encoding="utf-8",
|
|
123
|
+
filter=name_filter,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Cache the bound logger to prevent duplicate handlers
|
|
127
|
+
self._bound_loggers[self._name] = new_logger
|
|
128
|
+
|
|
129
|
+
return new_logger
|
|
130
|
+
|
|
131
|
+
def debug(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
132
|
+
"""Log a debug message."""
|
|
133
|
+
self._logger.debug(message, *args, **kwargs)
|
|
134
|
+
|
|
135
|
+
def info(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
136
|
+
"""Log an info message."""
|
|
137
|
+
self._logger.info(message, *args, **kwargs)
|
|
138
|
+
|
|
139
|
+
def warning(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
140
|
+
"""Log a warning message."""
|
|
141
|
+
self._logger.warning(message, *args, **kwargs)
|
|
142
|
+
|
|
143
|
+
def error(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
144
|
+
"""Log an error message."""
|
|
145
|
+
self._logger.error(message, *args, **kwargs)
|
|
146
|
+
|
|
147
|
+
def critical(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
148
|
+
"""Log a critical message."""
|
|
149
|
+
self._logger.critical(message, *args, **kwargs)
|
gpclog/manager.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# src/gpclog/manager.py
|
|
2
|
+
"""GPCLoggerManager - Singleton logger manager."""
|
|
3
|
+
|
|
4
|
+
from typing import ClassVar, Optional
|
|
5
|
+
from loguru import logger as loguru_logger
|
|
6
|
+
|
|
7
|
+
from gpconfig import GPConfigFolder
|
|
8
|
+
|
|
9
|
+
from gpclog.config import GPCLoggerConfig
|
|
10
|
+
from gpclog.logger import GPCLogger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GPCLoggerManager:
|
|
14
|
+
"""Singleton manager for GPCLogger instances.
|
|
15
|
+
|
|
16
|
+
This class manages all logger instances and provides a central
|
|
17
|
+
point for configuration management.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_instance: ClassVar[Optional["GPCLoggerManager"]] = None
|
|
21
|
+
|
|
22
|
+
def __new__(cls) -> "GPCLoggerManager":
|
|
23
|
+
"""Create or return the singleton instance."""
|
|
24
|
+
if cls._instance is None:
|
|
25
|
+
cls._instance = super().__new__(cls)
|
|
26
|
+
cls._instance._initialize()
|
|
27
|
+
loguru_logger.remove()
|
|
28
|
+
# Clear the bound loggers cache since handlers were removed
|
|
29
|
+
GPCLogger.clear_cache()
|
|
30
|
+
return cls._instance
|
|
31
|
+
|
|
32
|
+
def _initialize(self) -> None:
|
|
33
|
+
"""Initialize the manager instance."""
|
|
34
|
+
self._loggers: dict[tuple[str, Optional[int]], GPCLogger] = {}
|
|
35
|
+
self._config_folder: Optional[GPConfigFolder] = None
|
|
36
|
+
self._default_config: GPCLoggerConfig = self._create_default_config()
|
|
37
|
+
|
|
38
|
+
def _create_default_config(self) -> GPCLoggerConfig:
|
|
39
|
+
"""Create the default configuration.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
GPCLoggerConfig with default values.
|
|
43
|
+
"""
|
|
44
|
+
return GPCLoggerConfig(
|
|
45
|
+
name="default",
|
|
46
|
+
level="INFO",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def default_config(self) -> GPCLoggerConfig:
|
|
51
|
+
"""Get the default configuration."""
|
|
52
|
+
return self._default_config
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def config_folder(self) -> Optional[GPConfigFolder]:
|
|
56
|
+
"""Get the config folder."""
|
|
57
|
+
return self._config_folder
|
|
58
|
+
|
|
59
|
+
def set_config_folder(self, cfg_folder: GPConfigFolder) -> None:
|
|
60
|
+
"""Set the configuration folder for logger configs.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
cfg_folder: GPConfigFolder instance containing logger configurations.
|
|
64
|
+
"""
|
|
65
|
+
self._config_folder = cfg_folder
|
|
66
|
+
|
|
67
|
+
def get_logger(self, logger_name: str, sn: Optional[int] = None) -> GPCLogger:
|
|
68
|
+
"""Get or create a logger by name.
|
|
69
|
+
|
|
70
|
+
If a logger with the given name and sn already exists, returns the cached
|
|
71
|
+
instance. Otherwise, creates a new logger with the appropriate
|
|
72
|
+
configuration.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
logger_name: Unique name for the logger.
|
|
76
|
+
sn: Optional serial number. When provided, creates a logger named
|
|
77
|
+
"{logger_name}-{sn}" that uses the same configuration as
|
|
78
|
+
logger_name. Useful for multiprocess isolation.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
GPCLogger instance for the given name.
|
|
82
|
+
"""
|
|
83
|
+
cache_key = (logger_name, sn)
|
|
84
|
+
|
|
85
|
+
# Check cache first
|
|
86
|
+
if cache_key in self._loggers:
|
|
87
|
+
return self._loggers[cache_key]
|
|
88
|
+
|
|
89
|
+
# Get or create configuration using original logger_name
|
|
90
|
+
config = self._get_config(logger_name)
|
|
91
|
+
|
|
92
|
+
# Override name to include sn if provided
|
|
93
|
+
if sn is not None:
|
|
94
|
+
config = GPCLoggerConfig(
|
|
95
|
+
name=f"{logger_name}-{sn}",
|
|
96
|
+
level=config.level,
|
|
97
|
+
file_format=config.file_format,
|
|
98
|
+
console_format=config.console_format,
|
|
99
|
+
output_to_stdout=config.output_to_stdout,
|
|
100
|
+
output_to_stderr=config.output_to_stderr,
|
|
101
|
+
output_to_file=config.output_to_file,
|
|
102
|
+
log_path=config.log_path,
|
|
103
|
+
rotation_enabled=config.rotation_enabled,
|
|
104
|
+
rotation_size=config.rotation_size,
|
|
105
|
+
retention_enabled=config.retention_enabled,
|
|
106
|
+
retention_days=config.retention_days,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Create logger
|
|
110
|
+
logger = GPCLogger(config)
|
|
111
|
+
|
|
112
|
+
# Cache and return
|
|
113
|
+
self._loggers[cache_key] = logger
|
|
114
|
+
return logger
|
|
115
|
+
|
|
116
|
+
def _get_config(self, logger_name: str) -> GPCLoggerConfig:
|
|
117
|
+
"""Get configuration for a logger.
|
|
118
|
+
|
|
119
|
+
Tries to load configuration from config folder if set.
|
|
120
|
+
Falls back to default configuration if not found.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
logger_name: Name of the logger.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
GPCLoggerConfig for the logger.
|
|
127
|
+
"""
|
|
128
|
+
# Try to get config from config folder
|
|
129
|
+
if self._config_folder is not None:
|
|
130
|
+
try:
|
|
131
|
+
config = self._config_folder.get_config(logger_name, GPCLoggerConfig)
|
|
132
|
+
# Merge with defaults for missing fields
|
|
133
|
+
return self._merge_with_defaults(config)
|
|
134
|
+
except Exception:
|
|
135
|
+
# Config not found, try default.yaml
|
|
136
|
+
try:
|
|
137
|
+
default_config = self._config_folder.get_config(
|
|
138
|
+
"default", GPCLoggerConfig
|
|
139
|
+
)
|
|
140
|
+
self._default_config = self._merge_with_defaults(default_config)
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Use default config with the logger name
|
|
145
|
+
return GPCLoggerConfig(
|
|
146
|
+
name=logger_name,
|
|
147
|
+
level=self._default_config.level,
|
|
148
|
+
file_format=self._default_config.file_format,
|
|
149
|
+
console_format=self._default_config.console_format,
|
|
150
|
+
output_to_stdout=self._default_config.output_to_stdout,
|
|
151
|
+
output_to_stderr=self._default_config.output_to_stderr,
|
|
152
|
+
output_to_file=self._default_config.output_to_file,
|
|
153
|
+
log_path=self._default_config.log_path,
|
|
154
|
+
rotation_enabled=self._default_config.rotation_enabled,
|
|
155
|
+
rotation_size=self._default_config.rotation_size,
|
|
156
|
+
retention_enabled=self._default_config.retention_enabled,
|
|
157
|
+
retention_days=self._default_config.retention_days,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _merge_with_defaults(self, config: GPCLoggerConfig) -> GPCLoggerConfig:
|
|
161
|
+
"""Merge user config with defaults for missing fields.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
config: User-provided configuration.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
GPCLoggerConfig with all fields filled.
|
|
168
|
+
"""
|
|
169
|
+
defaults = self._default_config
|
|
170
|
+
return GPCLoggerConfig(
|
|
171
|
+
name=config.name,
|
|
172
|
+
level=config.level if config.level else defaults.level,
|
|
173
|
+
file_format=config.file_format
|
|
174
|
+
if config.file_format
|
|
175
|
+
else defaults.file_format,
|
|
176
|
+
console_format=config.console_format
|
|
177
|
+
if config.console_format
|
|
178
|
+
else defaults.console_format,
|
|
179
|
+
output_to_stdout=config.output_to_stdout
|
|
180
|
+
if config.output_to_stdout is not None
|
|
181
|
+
else defaults.output_to_stdout,
|
|
182
|
+
output_to_stderr=config.output_to_stderr
|
|
183
|
+
if config.output_to_stderr is not None
|
|
184
|
+
else defaults.output_to_stderr,
|
|
185
|
+
output_to_file=config.output_to_file
|
|
186
|
+
if config.output_to_file is not None
|
|
187
|
+
else defaults.output_to_file,
|
|
188
|
+
log_path=config.log_path if config.log_path else defaults.log_path,
|
|
189
|
+
rotation_enabled=config.rotation_enabled
|
|
190
|
+
if config.rotation_enabled is not None
|
|
191
|
+
else defaults.rotation_enabled,
|
|
192
|
+
rotation_size=config.rotation_size
|
|
193
|
+
if config.rotation_size
|
|
194
|
+
else defaults.rotation_size,
|
|
195
|
+
retention_enabled=config.retention_enabled
|
|
196
|
+
if config.retention_enabled is not None
|
|
197
|
+
else defaults.retention_enabled,
|
|
198
|
+
retention_days=config.retention_days
|
|
199
|
+
if config.retention_days is not None
|
|
200
|
+
else defaults.retention_days,
|
|
201
|
+
)
|
gpclog/utils.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Utility functions for gpclog."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_log_path(log_path: str) -> Path:
|
|
8
|
+
"""Resolve the log output path.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
log_path: Path specification. Can be:
|
|
12
|
+
- "auto": Auto-detect (GPCLOG_PATH env or home directory)
|
|
13
|
+
- "env": Use GPCLOG_PATH environment variable
|
|
14
|
+
- "home": Use user home directory
|
|
15
|
+
- Absolute path: Use specified path
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Resolved Path object pointing to the log output directory.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If path is invalid or cannot be resolved.
|
|
22
|
+
"""
|
|
23
|
+
base_path: Path
|
|
24
|
+
|
|
25
|
+
if log_path == "auto":
|
|
26
|
+
# Try GPCLOG_PATH first, then fall back to home
|
|
27
|
+
env_path = os.environ.get("GPCLOG_PATH")
|
|
28
|
+
if env_path:
|
|
29
|
+
base_path = Path(env_path)
|
|
30
|
+
else:
|
|
31
|
+
base_path = Path.home()
|
|
32
|
+
# Print message to user
|
|
33
|
+
print(
|
|
34
|
+
f"gpclog: Using home directory for logs: {base_path / 'gpclog_output'}"
|
|
35
|
+
)
|
|
36
|
+
elif log_path == "env":
|
|
37
|
+
env_path = os.environ.get("GPCLOG_PATH")
|
|
38
|
+
if not env_path:
|
|
39
|
+
raise ValueError("GPCLOG_PATH environment variable is not set")
|
|
40
|
+
base_path = Path(env_path)
|
|
41
|
+
elif log_path == "home":
|
|
42
|
+
base_path = Path.home()
|
|
43
|
+
else:
|
|
44
|
+
# Treat as absolute path
|
|
45
|
+
base_path = Path(log_path)
|
|
46
|
+
|
|
47
|
+
# Validate path exists and is a directory
|
|
48
|
+
if not base_path.exists():
|
|
49
|
+
raise ValueError(f"Log path does not exist: {base_path}")
|
|
50
|
+
if not base_path.is_dir():
|
|
51
|
+
raise ValueError(f"Log path is not a directory: {base_path}")
|
|
52
|
+
|
|
53
|
+
# Check if already named gpclog_output
|
|
54
|
+
if base_path.name == "gpclog_output":
|
|
55
|
+
return base_path
|
|
56
|
+
|
|
57
|
+
# Create gpclog_output subfolder
|
|
58
|
+
final_path = base_path / "gpclog_output"
|
|
59
|
+
final_path.mkdir(exist_ok=True)
|
|
60
|
+
return final_path
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gpclog
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: General Purpose Category Logger - A Python library for managing categorized logs in complex projects.
|
|
5
|
+
Project-URL: Homepage, https://github.com/linnetcodes/gpclog
|
|
6
|
+
Project-URL: Documentation, https://linnetcodes.github.io/gpclog
|
|
7
|
+
Project-URL: Repository, https://github.com/linnetcodes/gpclog
|
|
8
|
+
Project-URL: Issues, https://github.com/linnetcodes/gpclog/issues
|
|
9
|
+
Author-email: Linnet Codes <linnet.codes@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: categorized-logging,gpconfig,logging,loguru,python
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: System :: Logging
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: gpconfig>=0.3.0
|
|
29
|
+
Requires-Dist: loguru>=0.7.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# gpclog
|
|
37
|
+
|
|
38
|
+
[简体中文](README.zh-CN.md) | English
|
|
39
|
+
|
|
40
|
+
General Purpose Category Logger - A Python library for managing categorized logs in complex projects.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Category-based Logging** - Each category has its own configuration and file
|
|
45
|
+
- **Built on loguru** - High-performance, feature-rich logging core
|
|
46
|
+
- **Built on gpconfig** - Type-safe configuration management
|
|
47
|
+
- **Smart Defaults** - Works out of the box with zero configuration
|
|
48
|
+
- **Auto Class Registration** - Automatically registers with GPConfigManager on import
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install gpclog
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
### Basic Usage
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import gpclog
|
|
62
|
+
|
|
63
|
+
# Get a category logger (zero configuration)
|
|
64
|
+
logger = gpclog.get_logger("my_category")
|
|
65
|
+
logger.info("Hello, gpclog!")
|
|
66
|
+
# Output to: ~/gpclog_output/my_category.log
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Using Configuration Files
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import gpclog
|
|
73
|
+
from gpconfig import GPConfigManager
|
|
74
|
+
|
|
75
|
+
# Initialize GPConfigManager (GPCLoggerConfig and GPCLogger are auto-registered)
|
|
76
|
+
manager = GPConfigManager("myapp")
|
|
77
|
+
|
|
78
|
+
# Method 1: Using gpclog public API
|
|
79
|
+
logs_folder = manager.get_config("logs")
|
|
80
|
+
gpclog.set_config_folder(logs_folder)
|
|
81
|
+
logger = gpclog.get_logger("database")
|
|
82
|
+
|
|
83
|
+
# Method 2: Using GPConfigManager directly
|
|
84
|
+
logger = manager.get_object("logs.database")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Configuration schema, multiprocess usage, rotation/retention, and API details are documented in `docs/`.
|
|
88
|
+
|
|
89
|
+
## Documentation
|
|
90
|
+
|
|
91
|
+
Build and validate the documentation locally:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
venv/bin/mkdocs build --clean --strict
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Preview the documentation locally:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
venv/bin/mkdocs serve
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## API Reference
|
|
104
|
+
|
|
105
|
+
See:
|
|
106
|
+
|
|
107
|
+
- [Docs home](docs/index.md)
|
|
108
|
+
- [gpclog module](docs/gpclog.md)
|
|
109
|
+
- [GPCLogger](docs/logger.md)
|
|
110
|
+
- [GPCLoggerConfig](docs/config.md)
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
gpclog/__init__.py,sha256=eHjAjHVU8S5SlI0iEHZMYzlhgH7vUMS8wVvuxvLKO5k,2949
|
|
2
|
+
gpclog/config.py,sha256=s3q3c_NlO-VTkRZ9oPbJ1LtG_1q3RUvgh4jQ0-F3miw,1812
|
|
3
|
+
gpclog/logger.py,sha256=D2L8DgiQCJhu-rL9M4qDRw7hrrrxfUrCWEkXNbm4M54,5045
|
|
4
|
+
gpclog/manager.py,sha256=auCzaYL6W_spAG-OFnN5W01n8rFnx1gVudQiVTyvdGI,7757
|
|
5
|
+
gpclog/utils.py,sha256=26CqwnSZS44go8-O8GlllyrSkW1ZC3tU54jobfpCLkY,1927
|
|
6
|
+
gpclog-0.2.0.dist-info/METADATA,sha256=KH1eIB2bcU4RLb_2QsqK270JHkRcRMoVG2uWIhQ03P4,3287
|
|
7
|
+
gpclog-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
gpclog-0.2.0.dist-info/licenses/LICENSE,sha256=iQ52cpW4_bx-09PoR6JiM2vuI_WTgsCBve1G9-ydmgg,1094
|
|
9
|
+
gpclog-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Linnet Codes <linnet.codes@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|