taipanstack 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.
- taipanstack/__init__.py +53 -0
- taipanstack/config/__init__.py +25 -0
- taipanstack/config/generators.py +357 -0
- taipanstack/config/models.py +316 -0
- taipanstack/config/version_config.py +227 -0
- taipanstack/core/__init__.py +47 -0
- taipanstack/core/compat.py +329 -0
- taipanstack/core/optimizations.py +392 -0
- taipanstack/core/result.py +199 -0
- taipanstack/security/__init__.py +55 -0
- taipanstack/security/decorators.py +369 -0
- taipanstack/security/guards.py +362 -0
- taipanstack/security/sanitizers.py +321 -0
- taipanstack/security/validators.py +342 -0
- taipanstack/utils/__init__.py +24 -0
- taipanstack/utils/circuit_breaker.py +268 -0
- taipanstack/utils/filesystem.py +417 -0
- taipanstack/utils/logging.py +328 -0
- taipanstack/utils/metrics.py +272 -0
- taipanstack/utils/retry.py +300 -0
- taipanstack/utils/subprocess.py +344 -0
- taipanstack-0.1.0.dist-info/METADATA +350 -0
- taipanstack-0.1.0.dist-info/RECORD +25 -0
- taipanstack-0.1.0.dist-info/WHEEL +4 -0
- taipanstack-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured logging with context.
|
|
3
|
+
|
|
4
|
+
Provides a configured logger with support for structured output,
|
|
5
|
+
context propagation, and proper formatting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from typing import Any, Literal
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import structlog
|
|
18
|
+
|
|
19
|
+
HAS_STRUCTLOG = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
HAS_STRUCTLOG = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Default log format
|
|
25
|
+
DEFAULT_FORMAT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
|
26
|
+
JSON_FORMAT = (
|
|
27
|
+
'{"timestamp": "%(asctime)s", "level": "%(levelname)s", '
|
|
28
|
+
'"logger": "%(name)s", "message": "%(message)s"}'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StackLogger:
|
|
33
|
+
"""Enhanced logger with context support.
|
|
34
|
+
|
|
35
|
+
Provides a wrapper around standard logging with additional features
|
|
36
|
+
like context propagation and structured output support.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
name: Logger name.
|
|
40
|
+
level: Current log level.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
name: str = "stack",
|
|
47
|
+
level: str = "INFO",
|
|
48
|
+
*,
|
|
49
|
+
use_structured: bool = False,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Initialize the logger.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: Logger name.
|
|
55
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
|
|
56
|
+
use_structured: Use structured logging if structlog is available.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
self.name = name
|
|
60
|
+
self.level = level
|
|
61
|
+
self._context: dict[str, Any] = {}
|
|
62
|
+
|
|
63
|
+
if use_structured and HAS_STRUCTLOG:
|
|
64
|
+
self._logger = structlog.get_logger(name)
|
|
65
|
+
self._structured = True
|
|
66
|
+
else:
|
|
67
|
+
self._logger = logging.getLogger(name)
|
|
68
|
+
self._logger.setLevel(getattr(logging, level.upper()))
|
|
69
|
+
self._structured = False
|
|
70
|
+
|
|
71
|
+
def bind(self, **context: Any) -> StackLogger:
|
|
72
|
+
"""Add context to logger.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
**context: Key-value pairs to add to context.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Self for chaining.
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
self._context.update(context)
|
|
82
|
+
if self._structured and HAS_STRUCTLOG:
|
|
83
|
+
self._logger = self._logger.bind(**context)
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def unbind(self, *keys: str) -> StackLogger:
|
|
87
|
+
"""Remove context keys.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
*keys: Keys to remove from context.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Self for chaining.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
for key in keys:
|
|
97
|
+
self._context.pop(key, None)
|
|
98
|
+
if self._structured and HAS_STRUCTLOG:
|
|
99
|
+
self._logger = self._logger.unbind(*keys)
|
|
100
|
+
return self
|
|
101
|
+
|
|
102
|
+
def _format_message(self, message: str, **kwargs: Any) -> str:
|
|
103
|
+
"""Format message with context.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
message: The log message.
|
|
107
|
+
**kwargs: Additional context for this message.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Formatted message string.
|
|
111
|
+
|
|
112
|
+
"""
|
|
113
|
+
if not kwargs and not self._context:
|
|
114
|
+
return message
|
|
115
|
+
|
|
116
|
+
context = {**self._context, **kwargs}
|
|
117
|
+
context_str = " ".join(f"{k}={v}" for k, v in context.items())
|
|
118
|
+
return f"{message} | {context_str}"
|
|
119
|
+
|
|
120
|
+
def debug(self, message: str, **kwargs: Any) -> None:
|
|
121
|
+
"""Log a debug message.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
message: The message to log.
|
|
125
|
+
**kwargs: Additional context.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
if self._structured:
|
|
129
|
+
self._logger.debug(message, **kwargs)
|
|
130
|
+
else:
|
|
131
|
+
self._logger.debug(self._format_message(message, **kwargs))
|
|
132
|
+
|
|
133
|
+
def info(self, message: str, **kwargs: Any) -> None:
|
|
134
|
+
"""Log an info message.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
message: The message to log.
|
|
138
|
+
**kwargs: Additional context.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
if self._structured:
|
|
142
|
+
self._logger.info(message, **kwargs)
|
|
143
|
+
else:
|
|
144
|
+
self._logger.info(self._format_message(message, **kwargs))
|
|
145
|
+
|
|
146
|
+
def warning(self, message: str, **kwargs: Any) -> None:
|
|
147
|
+
"""Log a warning message.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
message: The message to log.
|
|
151
|
+
**kwargs: Additional context.
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
if self._structured:
|
|
155
|
+
self._logger.warning(message, **kwargs)
|
|
156
|
+
else:
|
|
157
|
+
self._logger.warning(self._format_message(message, **kwargs))
|
|
158
|
+
|
|
159
|
+
def error(self, message: str, **kwargs: Any) -> None:
|
|
160
|
+
"""Log an error message.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
message: The message to log.
|
|
164
|
+
**kwargs: Additional context.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
if self._structured:
|
|
168
|
+
self._logger.error(message, **kwargs)
|
|
169
|
+
else:
|
|
170
|
+
self._logger.error(self._format_message(message, **kwargs))
|
|
171
|
+
|
|
172
|
+
def critical(self, message: str, **kwargs: Any) -> None:
|
|
173
|
+
"""Log a critical message.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
message: The message to log.
|
|
177
|
+
**kwargs: Additional context.
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
if self._structured:
|
|
181
|
+
self._logger.critical(message, **kwargs)
|
|
182
|
+
else:
|
|
183
|
+
self._logger.critical(self._format_message(message, **kwargs))
|
|
184
|
+
|
|
185
|
+
def exception(self, message: str, **kwargs: Any) -> None:
|
|
186
|
+
"""Log an exception with traceback.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
message: The message to log.
|
|
190
|
+
**kwargs: Additional context.
|
|
191
|
+
|
|
192
|
+
"""
|
|
193
|
+
if self._structured:
|
|
194
|
+
self._logger.exception(message, **kwargs)
|
|
195
|
+
else:
|
|
196
|
+
self._logger.exception(self._format_message(message, **kwargs))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def setup_logging(
|
|
200
|
+
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO",
|
|
201
|
+
*,
|
|
202
|
+
format_type: Literal["simple", "detailed", "json"] = "detailed",
|
|
203
|
+
log_file: str | None = None,
|
|
204
|
+
use_structured: bool = False,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Configure the root logger.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
level: Log level to set.
|
|
210
|
+
format_type: Output format type.
|
|
211
|
+
log_file: Optional file to log to.
|
|
212
|
+
use_structured: Use structlog if available.
|
|
213
|
+
|
|
214
|
+
"""
|
|
215
|
+
# Configure structlog if available and requested
|
|
216
|
+
if use_structured and HAS_STRUCTLOG:
|
|
217
|
+
structlog.configure(
|
|
218
|
+
processors=[
|
|
219
|
+
structlog.stdlib.filter_by_level,
|
|
220
|
+
structlog.stdlib.add_logger_name,
|
|
221
|
+
structlog.stdlib.add_log_level,
|
|
222
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
223
|
+
structlog.processors.StackInfoRenderer(),
|
|
224
|
+
structlog.processors.format_exc_info,
|
|
225
|
+
structlog.processors.UnicodeDecoder(),
|
|
226
|
+
structlog.processors.JSONRenderer(),
|
|
227
|
+
],
|
|
228
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
229
|
+
context_class=dict,
|
|
230
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
231
|
+
)
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# Standard logging configuration
|
|
235
|
+
if format_type == "simple":
|
|
236
|
+
log_format = "%(levelname)s: %(message)s"
|
|
237
|
+
elif format_type == "json":
|
|
238
|
+
log_format = JSON_FORMAT
|
|
239
|
+
else:
|
|
240
|
+
log_format = DEFAULT_FORMAT
|
|
241
|
+
|
|
242
|
+
handlers: list[logging.Handler] = [logging.StreamHandler(sys.stdout)]
|
|
243
|
+
|
|
244
|
+
if log_file:
|
|
245
|
+
handlers.append(logging.FileHandler(log_file, encoding="utf-8"))
|
|
246
|
+
|
|
247
|
+
logging.basicConfig(
|
|
248
|
+
level=getattr(logging, level.upper()),
|
|
249
|
+
format=log_format,
|
|
250
|
+
handlers=handlers,
|
|
251
|
+
force=True,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_logger(
|
|
256
|
+
name: str = "stack",
|
|
257
|
+
*,
|
|
258
|
+
level: str = "INFO",
|
|
259
|
+
use_structured: bool = False,
|
|
260
|
+
) -> StackLogger:
|
|
261
|
+
"""Get a configured logger instance.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
name: Logger name.
|
|
265
|
+
level: Log level.
|
|
266
|
+
use_structured: Use structlog if available.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Configured StackLogger instance.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> logger = get_logger("my_module")
|
|
273
|
+
>>> logger.bind(request_id="123").info("Processing request")
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
return StackLogger(name, level, use_structured=use_structured)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def log_operation(
|
|
280
|
+
operation: str,
|
|
281
|
+
*,
|
|
282
|
+
logger: StackLogger | None = None,
|
|
283
|
+
level: str = "INFO",
|
|
284
|
+
) -> Any:
|
|
285
|
+
"""Context manager for logging operations.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
operation: Name of the operation.
|
|
289
|
+
logger: Logger to use (creates one if not provided).
|
|
290
|
+
level: Log level for messages.
|
|
291
|
+
|
|
292
|
+
Yields:
|
|
293
|
+
The logger instance.
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
>>> with log_operation("setup") as logger:
|
|
297
|
+
... logger.info("Setting up environment")
|
|
298
|
+
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
@contextmanager
|
|
302
|
+
def _log_context() -> Any:
|
|
303
|
+
nonlocal logger
|
|
304
|
+
if logger is None:
|
|
305
|
+
logger = get_logger()
|
|
306
|
+
|
|
307
|
+
start_time = datetime.now(UTC)
|
|
308
|
+
logger.bind(operation=operation)
|
|
309
|
+
|
|
310
|
+
log_method = getattr(logger, level.lower())
|
|
311
|
+
log_method(f"Starting: {operation}")
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
yield logger
|
|
315
|
+
duration = (datetime.now(UTC) - start_time).total_seconds()
|
|
316
|
+
log_method(f"Completed: {operation}", duration_seconds=duration)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
duration = (datetime.now(UTC) - start_time).total_seconds()
|
|
319
|
+
logger.exception(
|
|
320
|
+
f"Failed: {operation}",
|
|
321
|
+
duration_seconds=duration,
|
|
322
|
+
error=str(e),
|
|
323
|
+
)
|
|
324
|
+
raise
|
|
325
|
+
finally:
|
|
326
|
+
logger.unbind("operation")
|
|
327
|
+
|
|
328
|
+
return _log_context()
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metrics collection and monitoring utilities.
|
|
3
|
+
|
|
4
|
+
Provides lightweight metrics collection for monitoring
|
|
5
|
+
application performance and health. Compatible with any
|
|
6
|
+
Python framework.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Any, ParamSpec, TypeVar
|
|
19
|
+
|
|
20
|
+
P = ParamSpec("P")
|
|
21
|
+
R = TypeVar("R")
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("taipanstack.utils.metrics")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TimingStats:
|
|
28
|
+
"""Statistics for timing measurements."""
|
|
29
|
+
|
|
30
|
+
count: int = 0
|
|
31
|
+
total_time: float = 0.0
|
|
32
|
+
min_time: float = float("inf")
|
|
33
|
+
max_time: float = 0.0
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def avg_time(self) -> float:
|
|
37
|
+
"""Calculate average time."""
|
|
38
|
+
return self.total_time / self.count if self.count > 0 else 0.0
|
|
39
|
+
|
|
40
|
+
def record(self, duration: float) -> None:
|
|
41
|
+
"""Record a timing measurement."""
|
|
42
|
+
self.count += 1
|
|
43
|
+
self.total_time += duration
|
|
44
|
+
self.min_time = min(self.min_time, duration)
|
|
45
|
+
self.max_time = max(self.max_time, duration)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class Counter:
|
|
50
|
+
"""Simple counter metric."""
|
|
51
|
+
|
|
52
|
+
value: int = 0
|
|
53
|
+
lock: threading.Lock = field(default_factory=threading.Lock)
|
|
54
|
+
|
|
55
|
+
def increment(self, amount: int = 1) -> int:
|
|
56
|
+
"""Increment counter and return new value."""
|
|
57
|
+
with self.lock:
|
|
58
|
+
self.value += amount
|
|
59
|
+
return self.value
|
|
60
|
+
|
|
61
|
+
def decrement(self, amount: int = 1) -> int:
|
|
62
|
+
"""Decrement counter and return new value."""
|
|
63
|
+
with self.lock:
|
|
64
|
+
self.value -= amount
|
|
65
|
+
return self.value
|
|
66
|
+
|
|
67
|
+
def reset(self) -> None:
|
|
68
|
+
"""Reset counter to zero."""
|
|
69
|
+
with self.lock:
|
|
70
|
+
self.value = 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MetricsCollector:
|
|
74
|
+
"""Centralized metrics collection.
|
|
75
|
+
|
|
76
|
+
Thread-safe collector for various metric types including
|
|
77
|
+
counters, timers, and gauges.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
>>> metrics = MetricsCollector()
|
|
81
|
+
>>> metrics.increment("requests_total")
|
|
82
|
+
>>> with metrics.timer("request_duration"):
|
|
83
|
+
... process_request()
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_instance: MetricsCollector | None = None
|
|
88
|
+
_lock = threading.Lock()
|
|
89
|
+
_initialized: bool
|
|
90
|
+
|
|
91
|
+
def __new__(cls) -> MetricsCollector:
|
|
92
|
+
"""Singleton pattern for global metrics access."""
|
|
93
|
+
if cls._instance is None:
|
|
94
|
+
with cls._lock:
|
|
95
|
+
if cls._instance is None:
|
|
96
|
+
cls._instance = super().__new__(cls)
|
|
97
|
+
cls._instance._initialized = False
|
|
98
|
+
return cls._instance
|
|
99
|
+
|
|
100
|
+
def __init__(self) -> None:
|
|
101
|
+
"""Initialize metrics collector."""
|
|
102
|
+
if self._initialized:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self._counters: dict[str, Counter] = defaultdict(Counter)
|
|
106
|
+
self._timers: dict[str, TimingStats] = defaultdict(TimingStats)
|
|
107
|
+
self._gauges: dict[str, float] = {}
|
|
108
|
+
self._data_lock = threading.Lock()
|
|
109
|
+
self._initialized = True
|
|
110
|
+
|
|
111
|
+
def increment(self, name: str, amount: int = 1) -> int:
|
|
112
|
+
"""Increment a counter metric."""
|
|
113
|
+
return self._counters[name].increment(amount)
|
|
114
|
+
|
|
115
|
+
def decrement(self, name: str, amount: int = 1) -> int:
|
|
116
|
+
"""Decrement a counter metric."""
|
|
117
|
+
return self._counters[name].decrement(amount)
|
|
118
|
+
|
|
119
|
+
def gauge(self, name: str, value: float) -> None:
|
|
120
|
+
"""Set a gauge metric value."""
|
|
121
|
+
with self._data_lock:
|
|
122
|
+
self._gauges[name] = value
|
|
123
|
+
|
|
124
|
+
def get_gauge(self, name: str) -> float | None:
|
|
125
|
+
"""Get a gauge metric value."""
|
|
126
|
+
with self._data_lock:
|
|
127
|
+
return self._gauges.get(name)
|
|
128
|
+
|
|
129
|
+
def record_time(self, name: str, duration: float) -> None:
|
|
130
|
+
"""Record a timing measurement."""
|
|
131
|
+
self._timers[name].record(duration)
|
|
132
|
+
|
|
133
|
+
def timer(self, name: str) -> Timer:
|
|
134
|
+
"""Create a context manager timer."""
|
|
135
|
+
return Timer(name, self)
|
|
136
|
+
|
|
137
|
+
def get_counter(self, name: str) -> int:
|
|
138
|
+
"""Get current counter value."""
|
|
139
|
+
return self._counters[name].value
|
|
140
|
+
|
|
141
|
+
def get_timer_stats(self, name: str) -> TimingStats | None:
|
|
142
|
+
"""Get timing statistics for a named timer."""
|
|
143
|
+
return self._timers.get(name)
|
|
144
|
+
|
|
145
|
+
def get_all_metrics(self) -> dict[str, Any]:
|
|
146
|
+
"""Get all metrics as a dictionary."""
|
|
147
|
+
with self._data_lock:
|
|
148
|
+
return {
|
|
149
|
+
"counters": {k: v.value for k, v in self._counters.items()},
|
|
150
|
+
"timers": {
|
|
151
|
+
k: {
|
|
152
|
+
"count": v.count,
|
|
153
|
+
"avg": v.avg_time,
|
|
154
|
+
"min": v.min_time if v.count > 0 else 0,
|
|
155
|
+
"max": v.max_time,
|
|
156
|
+
"total": v.total_time,
|
|
157
|
+
}
|
|
158
|
+
for k, v in self._timers.items()
|
|
159
|
+
},
|
|
160
|
+
"gauges": dict(self._gauges),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def reset(self) -> None:
|
|
164
|
+
"""Reset all metrics."""
|
|
165
|
+
with self._data_lock:
|
|
166
|
+
self._counters.clear()
|
|
167
|
+
self._timers.clear()
|
|
168
|
+
self._gauges.clear()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Timer:
|
|
172
|
+
"""Context manager for timing code blocks."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, name: str, collector: MetricsCollector) -> None:
|
|
175
|
+
"""Initialize timer.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
name: Name of the timer metric.
|
|
179
|
+
collector: MetricsCollector to record to.
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
self.name = name
|
|
183
|
+
self.collector = collector
|
|
184
|
+
self.start_time: float = 0.0
|
|
185
|
+
|
|
186
|
+
def __enter__(self) -> Timer:
|
|
187
|
+
"""Start the timer."""
|
|
188
|
+
self.start_time = time.perf_counter()
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def __exit__(self, *args: Any) -> None:
|
|
192
|
+
"""Stop timer and record duration."""
|
|
193
|
+
duration = time.perf_counter() - self.start_time
|
|
194
|
+
self.collector.record_time(self.name, duration)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def timed(
|
|
198
|
+
name: str | None = None,
|
|
199
|
+
*,
|
|
200
|
+
collector: MetricsCollector | None = None,
|
|
201
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
202
|
+
"""Decorator to time function execution.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
name: Optional metric name (defaults to function name).
|
|
206
|
+
collector: Optional MetricsCollector instance.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Decorated function that records execution time.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> @timed("api_call_duration")
|
|
213
|
+
... def call_api(endpoint: str) -> dict:
|
|
214
|
+
... return requests.get(endpoint).json()
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
219
|
+
metric_name = name or func.__name__
|
|
220
|
+
metrics = collector or MetricsCollector()
|
|
221
|
+
|
|
222
|
+
@functools.wraps(func)
|
|
223
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
224
|
+
start = time.perf_counter()
|
|
225
|
+
try:
|
|
226
|
+
return func(*args, **kwargs)
|
|
227
|
+
finally:
|
|
228
|
+
duration = time.perf_counter() - start
|
|
229
|
+
metrics.record_time(metric_name, duration)
|
|
230
|
+
|
|
231
|
+
return wrapper
|
|
232
|
+
|
|
233
|
+
return decorator
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def counted(
|
|
237
|
+
name: str | None = None,
|
|
238
|
+
*,
|
|
239
|
+
collector: MetricsCollector | None = None,
|
|
240
|
+
) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
|
241
|
+
"""Decorator to count function calls.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
name: Optional metric name (defaults to function name).
|
|
245
|
+
collector: Optional MetricsCollector instance.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
Decorated function that counts calls.
|
|
249
|
+
|
|
250
|
+
Example:
|
|
251
|
+
>>> @counted("login_attempts")
|
|
252
|
+
... def login(username: str, password: str) -> bool:
|
|
253
|
+
... return authenticate(username, password)
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def decorator(func: Callable[P, R]) -> Callable[P, R]:
|
|
258
|
+
metric_name = name or f"{func.__name__}_calls"
|
|
259
|
+
metrics = collector or MetricsCollector()
|
|
260
|
+
|
|
261
|
+
@functools.wraps(func)
|
|
262
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
263
|
+
metrics.increment(metric_name)
|
|
264
|
+
return func(*args, **kwargs)
|
|
265
|
+
|
|
266
|
+
return wrapper
|
|
267
|
+
|
|
268
|
+
return decorator
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# Global metrics instance
|
|
272
|
+
metrics = MetricsCollector()
|