yb-observability 0.1.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.
- yb_observability/__init__.py +0 -0
- yb_observability/config/__init__.py +13 -0
- yb_observability/config/exceptions.py +10 -0
- yb_observability/config/loader.py +72 -0
- yb_observability/logging/__init__.py +3 -0
- yb_observability/logging/manager.py +51 -0
- yb_observability-0.1.0.dist-info/METADATA +203 -0
- yb_observability-0.1.0.dist-info/RECORD +10 -0
- yb_observability-0.1.0.dist-info/WHEEL +4 -0
- yb_observability-0.1.0.dist-info/licenses/LICENSE +0 -0
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from yb_observability.config.exceptions import (
|
|
2
|
+
ConfigEnvironmentVariableError,
|
|
3
|
+
ConfigFileNotFoundError,
|
|
4
|
+
ConfigLoaderError,
|
|
5
|
+
)
|
|
6
|
+
from yb_observability.config.loader import ConfigLoader
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ConfigEnvironmentVariableError",
|
|
10
|
+
"ConfigFileNotFoundError",
|
|
11
|
+
"ConfigLoader",
|
|
12
|
+
"ConfigLoaderError",
|
|
13
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class ConfigLoaderError(Exception):
|
|
2
|
+
"""Base exception for configuration loading errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ConfigFileNotFoundError(ConfigLoaderError):
|
|
6
|
+
"""Raised when the configuration file cannot be found."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigEnvironmentVariableError(ConfigLoaderError):
|
|
10
|
+
"""Raised when a required environment variable is missing."""
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from os import getenv
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from re import Match, compile
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from yaml import YAMLError, safe_load
|
|
7
|
+
|
|
8
|
+
from yb_observability.config.exceptions import (
|
|
9
|
+
ConfigEnvironmentVariableError,
|
|
10
|
+
ConfigFileNotFoundError,
|
|
11
|
+
ConfigLoaderError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_ENV_VAR_PATTERN = compile(r"\$\{([A-Za-z][A-Za-z0-9_]*)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigLoader:
|
|
18
|
+
"""Load YAML configuration and resolve environment variables."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, config_path: str | Path) -> None:
|
|
21
|
+
self.config_path = Path(config_path)
|
|
22
|
+
|
|
23
|
+
def load(self) -> dict[str, Any]:
|
|
24
|
+
"""Load and resolve the configuration file."""
|
|
25
|
+
|
|
26
|
+
if not self.config_path.is_file():
|
|
27
|
+
raise ConfigFileNotFoundError(
|
|
28
|
+
f"Configuration file not found: {self.config_path}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
content = self.config_path.read_text(encoding="utf-8")
|
|
33
|
+
except OSError as exc:
|
|
34
|
+
raise ConfigLoaderError(
|
|
35
|
+
f"Unable to read configuration file: {self.config_path}"
|
|
36
|
+
) from exc
|
|
37
|
+
|
|
38
|
+
resolved_content = self._resolve_environment_variables(content)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
config = safe_load(resolved_content)
|
|
42
|
+
except YAMLError as exc:
|
|
43
|
+
raise ConfigLoaderError(
|
|
44
|
+
f"Invalid YAML configuration: {self.config_path}"
|
|
45
|
+
) from exc
|
|
46
|
+
|
|
47
|
+
if config is None:
|
|
48
|
+
return {}
|
|
49
|
+
|
|
50
|
+
if not isinstance(config, dict):
|
|
51
|
+
raise ConfigLoaderError(
|
|
52
|
+
"The root of the configuration file must be a YAML mapping."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return config
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _resolve_environment_variables(content: str) -> str:
|
|
59
|
+
"""Resolve ${ENV_VAR} references using environment variables."""
|
|
60
|
+
|
|
61
|
+
def replace(match: Match[str]) -> str:
|
|
62
|
+
variable_name = match.group(1)
|
|
63
|
+
value = getenv(variable_name)
|
|
64
|
+
|
|
65
|
+
if value is None:
|
|
66
|
+
raise ConfigEnvironmentVariableError(
|
|
67
|
+
f"Required environment variable is not set: {variable_name}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
return _ENV_VAR_PATTERN.sub(replace, content)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
_LOG_FORMAT = (
|
|
6
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|
7
|
+
"<level>{level: <5}</level> | "
|
|
8
|
+
"<cyan>[{extra[service]: <10}]</cyan> | "
|
|
9
|
+
"<blue>{name}:{function}:{line}</blue>\n"
|
|
10
|
+
"<level>| {message}</level>"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_logging(
|
|
15
|
+
service: str,
|
|
16
|
+
*,
|
|
17
|
+
log_directory: str | Path = "logs",
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Configure application logging for a service.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
service: Name identifying the application or service.
|
|
23
|
+
log_directory: Root directory used for log files.
|
|
24
|
+
"""
|
|
25
|
+
if not service.strip():
|
|
26
|
+
raise ValueError("Service name cannot be empty.")
|
|
27
|
+
|
|
28
|
+
log_path = Path(log_directory) / service
|
|
29
|
+
log_path.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
logger.remove()
|
|
32
|
+
|
|
33
|
+
logger.configure(
|
|
34
|
+
extra={"service": service.upper()},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger.add(
|
|
38
|
+
sink=lambda message: print(message, end=""),
|
|
39
|
+
format=_LOG_FORMAT,
|
|
40
|
+
level="DEBUG",
|
|
41
|
+
colorize=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
logger.add(
|
|
45
|
+
log_path / "{time:YYYY-MM-DD}.log",
|
|
46
|
+
format=_LOG_FORMAT,
|
|
47
|
+
level="DEBUG",
|
|
48
|
+
rotation="00:00",
|
|
49
|
+
retention="30 days",
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: yb-observability
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reusable Python infrastructure for application configuration and observability.
|
|
5
|
+
Author: Yassine Bouali
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: <3.15,>=3.14
|
|
8
|
+
Requires-Dist: loguru<1.0.0,>=0.7.3
|
|
9
|
+
Requires-Dist: pyyaml<7.0,>=6.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# yb-observability
|
|
13
|
+
|
|
14
|
+
Reusable Python infrastructure for application configuration and observability.
|
|
15
|
+
|
|
16
|
+
`yb-observability` provides common infrastructure that can be shared across Python applications without coupling the library to a specific application or architecture.
|
|
17
|
+
|
|
18
|
+
The project currently focuses on configuration and logging, with additional observability capabilities planned for future releases.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
### Configuration
|
|
23
|
+
|
|
24
|
+
* YAML configuration loading.
|
|
25
|
+
* Environment variable resolution.
|
|
26
|
+
* Nested environment variable support.
|
|
27
|
+
* Configuration validation.
|
|
28
|
+
* Clear configuration-related exceptions.
|
|
29
|
+
|
|
30
|
+
### Logging
|
|
31
|
+
|
|
32
|
+
* Loguru-based logging.
|
|
33
|
+
* Explicit application/service identity.
|
|
34
|
+
* Consistent human-readable log format.
|
|
35
|
+
* Console logging.
|
|
36
|
+
* File logging.
|
|
37
|
+
* Daily log rotation.
|
|
38
|
+
* Configurable retention.
|
|
39
|
+
* Per-service log directories.
|
|
40
|
+
|
|
41
|
+
### Future
|
|
42
|
+
|
|
43
|
+
The library is designed to grow with additional infrastructure such as:
|
|
44
|
+
|
|
45
|
+
* Structured/JSON logging.
|
|
46
|
+
* Request and correlation IDs.
|
|
47
|
+
* Metrics.
|
|
48
|
+
* Distributed tracing.
|
|
49
|
+
* OpenTelemetry integrations.
|
|
50
|
+
* Additional observability integrations.
|
|
51
|
+
|
|
52
|
+
Features are introduced incrementally and remain independent where possible.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Install the package from PyPI:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install yb-observability
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
With `uv`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add yb-observability
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Basic Usage
|
|
69
|
+
|
|
70
|
+
Configuration:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from yb_observability.config import ConfigLoader
|
|
74
|
+
|
|
75
|
+
config = ConfigLoader("config.yml").load()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Logging:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from yb_observability.logging import configure_logging
|
|
82
|
+
|
|
83
|
+
configure_logging(service="my-service")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The application remains responsible for deciding what should be logged. `yb-observability` is responsible for configuring the logging infrastructure and providing a consistent logging interface.
|
|
87
|
+
|
|
88
|
+
## Design Goals
|
|
89
|
+
|
|
90
|
+
### Reusable
|
|
91
|
+
|
|
92
|
+
The library must not depend on a specific application, framework, infrastructure provider, or domain.
|
|
93
|
+
|
|
94
|
+
### Explicit
|
|
95
|
+
|
|
96
|
+
Applications should explicitly configure the behavior they require rather than relying on hidden conventions.
|
|
97
|
+
|
|
98
|
+
### Consistent
|
|
99
|
+
|
|
100
|
+
Applications using the library should receive consistent configuration and logging behavior.
|
|
101
|
+
|
|
102
|
+
### Extensible
|
|
103
|
+
|
|
104
|
+
New capabilities should be possible without breaking the existing public API.
|
|
105
|
+
|
|
106
|
+
### Testable
|
|
107
|
+
|
|
108
|
+
Core functionality should be covered by automated tests.
|
|
109
|
+
|
|
110
|
+
### Production-oriented
|
|
111
|
+
|
|
112
|
+
The library should be suitable for both local development and production applications.
|
|
113
|
+
|
|
114
|
+
## Project Structure
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
yb-observability/
|
|
118
|
+
├── src/
|
|
119
|
+
│ └── yb_observability/
|
|
120
|
+
│ ├── config/
|
|
121
|
+
│ └── logging/
|
|
122
|
+
│
|
|
123
|
+
├── tests/
|
|
124
|
+
│ ├── config/
|
|
125
|
+
│ └── logging/
|
|
126
|
+
│
|
|
127
|
+
├── docs/
|
|
128
|
+
│ ├── architecture/
|
|
129
|
+
│ ├── configuration/
|
|
130
|
+
│ └── logging/
|
|
131
|
+
│
|
|
132
|
+
├── .github/
|
|
133
|
+
│ └── workflows/
|
|
134
|
+
│
|
|
135
|
+
├── CHANGELOG.md
|
|
136
|
+
├── CODE_OF_CONDUCT.md
|
|
137
|
+
├── CONTRIBUTING.md
|
|
138
|
+
├── LICENSE
|
|
139
|
+
├── README.md
|
|
140
|
+
├── SECURITY.md
|
|
141
|
+
└── pyproject.toml
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
The project uses:
|
|
147
|
+
|
|
148
|
+
* Python 3.14
|
|
149
|
+
* uv
|
|
150
|
+
* Ruff
|
|
151
|
+
* Pyright
|
|
152
|
+
* Pytest
|
|
153
|
+
* Loguru
|
|
154
|
+
* PyYAML
|
|
155
|
+
|
|
156
|
+
Install development dependencies:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv sync
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Run the test suite:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv run pytest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Run linting:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv run ruff check .
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Check formatting:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uv run ruff format --check .
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Run type checking:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
uv run pyright
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
Contributions are welcome.
|
|
189
|
+
|
|
190
|
+
Before submitting a pull request, make sure that:
|
|
191
|
+
|
|
192
|
+
* Tests pass.
|
|
193
|
+
* Ruff checks pass.
|
|
194
|
+
* Formatting checks pass.
|
|
195
|
+
* Pyright checks pass.
|
|
196
|
+
* New functionality includes appropriate tests.
|
|
197
|
+
* Public API changes are documented.
|
|
198
|
+
|
|
199
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development and contribution guidelines.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
This project is licensed under the terms defined in [LICENSE](LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
yb_observability/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
yb_observability/config/__init__.py,sha256=uQ3gLWWL0__YYRIn9Dit4mZ0YerKgCqgmvjFlfFzQw0,324
|
|
3
|
+
yb_observability/config/exceptions.py,sha256=DUaxgVVOyveg5CcYqp115_FObDBu3sjRdZKvT6jGj-4,334
|
|
4
|
+
yb_observability/config/loader.py,sha256=Ad9L4HxBOxlWNb7ic5I6qFnZr9jtx-0WfKDQslSQh3g,2132
|
|
5
|
+
yb_observability/logging/__init__.py,sha256=bvTUA55YxHhw9z2Ba82REmPTHta7oyyHP9q7jFPhxZw,96
|
|
6
|
+
yb_observability/logging/manager.py,sha256=1r-lXPmg7NX1fWrMKwVAg3X3Gh62EKF82UglCbhGMHk,1202
|
|
7
|
+
yb_observability-0.1.0.dist-info/METADATA,sha256=i4vSmiLhyQ68xkuzT55VNpMO4Z2-NggeDiSTvucV5PI,4094
|
|
8
|
+
yb_observability-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
yb_observability-0.1.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
yb_observability-0.1.0.dist-info/RECORD,,
|
|
File without changes
|