crackerjack 0.30.3__py3-none-any.whl → 0.31.4__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +225 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +169 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +652 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +401 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +561 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +640 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +411 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +435 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +144 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +615 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +370 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +141 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +360 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +347 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +347 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +395 -0
- crackerjack/services/git.py +165 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +847 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.4.dist-info/METADATA +742 -0
- crackerjack-0.31.4.dist-info/RECORD +148 -0
- crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import time
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogManager:
|
|
14
|
+
def __init__(self, app_name: str = "crackerjack") -> None:
|
|
15
|
+
self.app_name = app_name
|
|
16
|
+
self._log_dir = self._get_log_directory()
|
|
17
|
+
self._setup_directories()
|
|
18
|
+
|
|
19
|
+
def _get_log_directory(self) -> Path:
|
|
20
|
+
cache_home = os.environ.get("XDG_CACHE_HOME")
|
|
21
|
+
base_dir = Path(cache_home) if cache_home else Path.home() / ".cache"
|
|
22
|
+
|
|
23
|
+
return base_dir / self.app_name / "logs"
|
|
24
|
+
|
|
25
|
+
def _setup_directories(self) -> None:
|
|
26
|
+
self._log_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
(self._log_dir / "debug").mkdir(exist_ok=True)
|
|
29
|
+
(self._log_dir / "error").mkdir(exist_ok=True)
|
|
30
|
+
(self._log_dir / "audit").mkdir(exist_ok=True)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def log_dir(self) -> Path:
|
|
34
|
+
return self._log_dir
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def debug_dir(self) -> Path:
|
|
38
|
+
return self._log_dir / "debug"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def error_dir(self) -> Path:
|
|
42
|
+
return self._log_dir / "error"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def audit_dir(self) -> Path:
|
|
46
|
+
return self._log_dir / "audit"
|
|
47
|
+
|
|
48
|
+
def create_debug_log_file(self, session_id: str | None = None) -> Path:
|
|
49
|
+
timestamp = int(time.time())
|
|
50
|
+
if session_id:
|
|
51
|
+
filename = f"debug-{timestamp}-{session_id}.log"
|
|
52
|
+
else:
|
|
53
|
+
filename = f"debug-{timestamp}.log"
|
|
54
|
+
|
|
55
|
+
return self.debug_dir / filename
|
|
56
|
+
|
|
57
|
+
def create_error_log_file(self, error_type: str = "general") -> Path:
|
|
58
|
+
timestamp = int(time.time())
|
|
59
|
+
filename = f"error-{error_type}-{timestamp}.log"
|
|
60
|
+
return self.error_dir / filename
|
|
61
|
+
|
|
62
|
+
def rotate_logs(
|
|
63
|
+
self,
|
|
64
|
+
log_dir: Path,
|
|
65
|
+
pattern: str,
|
|
66
|
+
max_files: int = 10,
|
|
67
|
+
max_age_days: int = 30,
|
|
68
|
+
) -> int:
|
|
69
|
+
if not log_dir.exists():
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
removed_count = 0
|
|
73
|
+
cutoff_time = time.time() - (max_age_days * 24 * 60 * 60)
|
|
74
|
+
|
|
75
|
+
log_files = sorted(
|
|
76
|
+
log_dir.glob(pattern),
|
|
77
|
+
key=lambda p: p.stat().st_mtime,
|
|
78
|
+
reverse=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
for i, log_file in enumerate(log_files):
|
|
82
|
+
should_remove = False
|
|
83
|
+
reason = ""
|
|
84
|
+
|
|
85
|
+
if i >= max_files:
|
|
86
|
+
should_remove = True
|
|
87
|
+
reason = f"exceeds max files ({max_files})"
|
|
88
|
+
|
|
89
|
+
elif log_file.stat().st_mtime < cutoff_time:
|
|
90
|
+
should_remove = True
|
|
91
|
+
reason = f"older than {max_age_days} days"
|
|
92
|
+
|
|
93
|
+
if should_remove:
|
|
94
|
+
with suppress(OSError, PermissionError):
|
|
95
|
+
log_file.unlink()
|
|
96
|
+
removed_count += 1
|
|
97
|
+
console.print(
|
|
98
|
+
f"[dim]🗑️ Removed old log: {log_file.name} ({reason})[/dim]",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return removed_count
|
|
102
|
+
|
|
103
|
+
def cleanup_all_logs(
|
|
104
|
+
self,
|
|
105
|
+
debug_max_files: int = 10,
|
|
106
|
+
error_max_files: int = 20,
|
|
107
|
+
audit_max_files: int = 50,
|
|
108
|
+
max_age_days: int = 30,
|
|
109
|
+
) -> dict[str, int]:
|
|
110
|
+
results = {}
|
|
111
|
+
|
|
112
|
+
results["debug"] = self.rotate_logs(
|
|
113
|
+
self.debug_dir,
|
|
114
|
+
"debug-*.log",
|
|
115
|
+
debug_max_files,
|
|
116
|
+
max_age_days,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
results["error"] = self.rotate_logs(
|
|
120
|
+
self.error_dir,
|
|
121
|
+
"error-*.log",
|
|
122
|
+
error_max_files,
|
|
123
|
+
max_age_days,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
results["audit"] = self.rotate_logs(
|
|
127
|
+
self.audit_dir,
|
|
128
|
+
"audit-*.log",
|
|
129
|
+
audit_max_files,
|
|
130
|
+
max_age_days,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return results
|
|
134
|
+
|
|
135
|
+
def migrate_legacy_logs(
|
|
136
|
+
self,
|
|
137
|
+
source_dir: Path,
|
|
138
|
+
dry_run: bool = False,
|
|
139
|
+
) -> dict[str, int]:
|
|
140
|
+
if not source_dir.exists():
|
|
141
|
+
return {"moved": 0, "failed": 0, "found": 0}
|
|
142
|
+
|
|
143
|
+
debug_pattern = "crackerjack-debug-*.log"
|
|
144
|
+
legacy_files = list(source_dir.glob(debug_pattern))
|
|
145
|
+
|
|
146
|
+
results = {"found": len(legacy_files), "moved": 0, "failed": 0}
|
|
147
|
+
|
|
148
|
+
if not legacy_files:
|
|
149
|
+
console.print("[green]✅ No legacy log files found to migrate[/green]")
|
|
150
|
+
return results
|
|
151
|
+
|
|
152
|
+
console.print(
|
|
153
|
+
f"[yellow]📦 Found {len(legacy_files)} legacy debug log files to migrate[/yellow]",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
for legacy_file in legacy_files:
|
|
157
|
+
try:
|
|
158
|
+
parts = legacy_file.stem.split(" - ")
|
|
159
|
+
if len(parts) >= 3 and parts[-1].isdigit():
|
|
160
|
+
timestamp = parts[-1]
|
|
161
|
+
else:
|
|
162
|
+
timestamp = str(int(legacy_file.stat().st_mtime))
|
|
163
|
+
|
|
164
|
+
new_filename = f"debug-{timestamp}-migrated.log"
|
|
165
|
+
new_path = self.debug_dir / new_filename
|
|
166
|
+
|
|
167
|
+
if dry_run:
|
|
168
|
+
console.print(
|
|
169
|
+
f"[cyan]📋 Would move: {legacy_file.name} → {new_filename}[/cyan]",
|
|
170
|
+
)
|
|
171
|
+
results["moved"] += 1
|
|
172
|
+
else:
|
|
173
|
+
shutil.move(str(legacy_file), str(new_path))
|
|
174
|
+
results["moved"] += 1
|
|
175
|
+
console.print(
|
|
176
|
+
f"[green]✅ Moved: {legacy_file.name} → {new_filename}[/green]",
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
except (OSError, ValueError) as e:
|
|
180
|
+
results["failed"] += 1
|
|
181
|
+
console.print(
|
|
182
|
+
f"[red]❌ Failed to migrate {legacy_file.name}: {e}[/red]",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
def get_log_stats(self) -> dict[str, dict[str, int | str]]:
|
|
188
|
+
stats = {}
|
|
189
|
+
|
|
190
|
+
for log_type, log_dir in (
|
|
191
|
+
("debug", self.debug_dir),
|
|
192
|
+
("error", self.error_dir),
|
|
193
|
+
("audit", self.audit_dir),
|
|
194
|
+
):
|
|
195
|
+
if log_dir.exists():
|
|
196
|
+
files = list(log_dir.glob("*.log"))
|
|
197
|
+
total_size = sum(f.stat().st_size for f in files if f.exists())
|
|
198
|
+
|
|
199
|
+
stats[log_type] = {
|
|
200
|
+
"count": len(files),
|
|
201
|
+
"size_mb": round(total_size / (1024 * 1024), 2),
|
|
202
|
+
"location": str(log_dir),
|
|
203
|
+
}
|
|
204
|
+
else:
|
|
205
|
+
stats[log_type] = {
|
|
206
|
+
"count": 0,
|
|
207
|
+
"size_mb": 0.0,
|
|
208
|
+
"location": str(log_dir),
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return stats
|
|
212
|
+
|
|
213
|
+
def setup_rotating_file_handler(
|
|
214
|
+
self,
|
|
215
|
+
logger: logging.Logger,
|
|
216
|
+
log_type: str = "debug",
|
|
217
|
+
max_bytes: int = 10 * 1024 * 1024,
|
|
218
|
+
backup_count: int = 5,
|
|
219
|
+
) -> logging.FileHandler:
|
|
220
|
+
from logging.handlers import RotatingFileHandler
|
|
221
|
+
|
|
222
|
+
if log_type == "error":
|
|
223
|
+
log_dir = self.error_dir
|
|
224
|
+
elif log_type == "audit":
|
|
225
|
+
log_dir = self.audit_dir
|
|
226
|
+
else:
|
|
227
|
+
log_dir = self.debug_dir
|
|
228
|
+
|
|
229
|
+
log_file = log_dir / f"{log_type}.log"
|
|
230
|
+
|
|
231
|
+
handler = RotatingFileHandler(
|
|
232
|
+
log_file,
|
|
233
|
+
maxBytes=max_bytes,
|
|
234
|
+
backupCount=backup_count,
|
|
235
|
+
encoding="utf-8",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
formatter = logging.Formatter(
|
|
239
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
240
|
+
)
|
|
241
|
+
handler.setFormatter(formatter)
|
|
242
|
+
|
|
243
|
+
logger.addHandler(handler)
|
|
244
|
+
|
|
245
|
+
return handler
|
|
246
|
+
|
|
247
|
+
def print_log_summary(self) -> None:
|
|
248
|
+
stats = self.get_log_stats()
|
|
249
|
+
|
|
250
|
+
console.print("\n[bold]📊 Log File Summary[/bold]")
|
|
251
|
+
console.print(f"[dim]Location: {self.log_dir}[/dim]")
|
|
252
|
+
|
|
253
|
+
total_files = 0
|
|
254
|
+
total_size = 0.0
|
|
255
|
+
|
|
256
|
+
for log_type, data in stats.items():
|
|
257
|
+
count = (
|
|
258
|
+
int(data["count"]) if isinstance(data["count"], str) else data["count"]
|
|
259
|
+
)
|
|
260
|
+
size = (
|
|
261
|
+
float(data["size_mb"])
|
|
262
|
+
if isinstance(data["size_mb"], str)
|
|
263
|
+
else data["size_mb"]
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
total_files += count
|
|
267
|
+
total_size += size
|
|
268
|
+
|
|
269
|
+
if count > 0:
|
|
270
|
+
console.print(f" {log_type.capitalize()}: {count} files, {size}MB")
|
|
271
|
+
else:
|
|
272
|
+
console.print(f" {log_type.capitalize()}: No files")
|
|
273
|
+
|
|
274
|
+
if total_files > 0:
|
|
275
|
+
console.print(
|
|
276
|
+
f"\n[bold]Total: {total_files} files, {total_size:.2f}MB[/bold]",
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
console.print("\n[dim]No log files found[/dim]")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
log_manager = LogManager()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_log_manager() -> LogManager:
|
|
286
|
+
return log_manager
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import typing as t
|
|
5
|
+
import uuid
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
correlation_id: ContextVar[str | None] = ContextVar("correlation_id", default=None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_correlation_id(cid: str) -> None:
|
|
17
|
+
correlation_id.set(cid)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_correlation_id() -> str:
|
|
21
|
+
cid = correlation_id.get()
|
|
22
|
+
if cid is None:
|
|
23
|
+
cid = str(uuid.uuid4())[:8]
|
|
24
|
+
correlation_id.set(cid)
|
|
25
|
+
return cid
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_correlation_id(_: Any, __: Any, event_dict: dict[str, Any]) -> dict[str, Any]:
|
|
29
|
+
event_dict["correlation_id"] = get_correlation_id()
|
|
30
|
+
return event_dict
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def add_timestamp(_: Any, __: Any, event_dict: dict[str, Any]) -> dict[str, Any]:
|
|
34
|
+
event_dict["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
35
|
+
return event_dict
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def setup_structured_logging(
|
|
39
|
+
level: str = "INFO",
|
|
40
|
+
json_output: bool = True,
|
|
41
|
+
log_file: Path | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
processors = [
|
|
44
|
+
structlog.stdlib.filter_by_level,
|
|
45
|
+
add_timestamp,
|
|
46
|
+
add_correlation_id,
|
|
47
|
+
structlog.stdlib.add_logger_name,
|
|
48
|
+
structlog.stdlib.add_log_level,
|
|
49
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
50
|
+
structlog.processors.StackInfoRenderer(),
|
|
51
|
+
structlog.processors.format_exc_info,
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
if json_output:
|
|
55
|
+
processors.append(structlog.processors.JSONRenderer()) # type: ignore[arg-type]
|
|
56
|
+
else:
|
|
57
|
+
processors.append(structlog.dev.ConsoleRenderer(colors=True)) # type: ignore[arg-type]
|
|
58
|
+
|
|
59
|
+
structlog.configure(
|
|
60
|
+
processors=processors, # type: ignore[arg-type]
|
|
61
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
62
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
63
|
+
cache_logger_on_first_use=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
log_level = getattr(logging, level.upper())
|
|
67
|
+
|
|
68
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
69
|
+
console_handler.setLevel(log_level)
|
|
70
|
+
|
|
71
|
+
handlers = [console_handler]
|
|
72
|
+
|
|
73
|
+
if log_file:
|
|
74
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
file_handler = logging.FileHandler(log_file)
|
|
76
|
+
file_handler.setLevel(log_level)
|
|
77
|
+
handlers.append(file_handler)
|
|
78
|
+
|
|
79
|
+
logging.basicConfig(
|
|
80
|
+
level=log_level,
|
|
81
|
+
handlers=handlers,
|
|
82
|
+
format="%(message)s",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_logger(name: str) -> structlog.BoundLogger:
|
|
87
|
+
return structlog.get_logger(name)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LoggingContext:
|
|
91
|
+
def __init__(self, operation: str, **kwargs: Any) -> None:
|
|
92
|
+
self.operation = operation
|
|
93
|
+
self.kwargs = kwargs
|
|
94
|
+
self.correlation_id = str(uuid.uuid4())[:8]
|
|
95
|
+
self.logger = get_logger("crackerjack.context")
|
|
96
|
+
self.start_time = time.time()
|
|
97
|
+
|
|
98
|
+
def __enter__(self) -> str:
|
|
99
|
+
set_correlation_id(self.correlation_id)
|
|
100
|
+
self.logger.info("Operation started", operation=self.operation, **self.kwargs)
|
|
101
|
+
return self.correlation_id
|
|
102
|
+
|
|
103
|
+
def __exit__(
|
|
104
|
+
self,
|
|
105
|
+
exc_type: type[BaseException] | None,
|
|
106
|
+
exc_val: BaseException | None,
|
|
107
|
+
_: TracebackType | None,
|
|
108
|
+
) -> None:
|
|
109
|
+
duration = time.time() - self.start_time
|
|
110
|
+
|
|
111
|
+
if exc_type is None:
|
|
112
|
+
self.logger.info(
|
|
113
|
+
"Operation completed",
|
|
114
|
+
operation=self.operation,
|
|
115
|
+
duration_seconds=round(duration, 3),
|
|
116
|
+
**self.kwargs,
|
|
117
|
+
)
|
|
118
|
+
else:
|
|
119
|
+
self.logger.error(
|
|
120
|
+
"Operation failed",
|
|
121
|
+
operation=self.operation,
|
|
122
|
+
duration_seconds=round(duration, 3),
|
|
123
|
+
error=str(exc_val),
|
|
124
|
+
error_type=exc_type.__name__ if exc_type else None,
|
|
125
|
+
**self.kwargs,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def log_performance(
|
|
130
|
+
operation: str,
|
|
131
|
+
**kwargs: Any,
|
|
132
|
+
) -> t.Callable[[t.Callable[..., t.Any]], t.Callable[..., t.Any]]:
|
|
133
|
+
def decorator(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
|
|
134
|
+
def wrapper(*args: t.Any, **func_kwargs: t.Any) -> t.Any:
|
|
135
|
+
logger = get_logger(f"crackerjack.perf.{func.__name__}")
|
|
136
|
+
start_time = time.time()
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
result = func(*args, **func_kwargs)
|
|
140
|
+
duration = time.time() - start_time
|
|
141
|
+
logger.info(
|
|
142
|
+
"Function completed",
|
|
143
|
+
operation=operation,
|
|
144
|
+
function=func.__name__,
|
|
145
|
+
duration_seconds=round(duration, 3),
|
|
146
|
+
success=True,
|
|
147
|
+
**kwargs,
|
|
148
|
+
)
|
|
149
|
+
return result
|
|
150
|
+
except Exception as e:
|
|
151
|
+
duration = time.time() - start_time
|
|
152
|
+
logger.exception(
|
|
153
|
+
"Function failed",
|
|
154
|
+
operation=operation,
|
|
155
|
+
function=func.__name__,
|
|
156
|
+
duration_seconds=round(duration, 3),
|
|
157
|
+
success=False,
|
|
158
|
+
error=str(e),
|
|
159
|
+
error_type=type(e).__name__,
|
|
160
|
+
**kwargs,
|
|
161
|
+
)
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
return wrapper
|
|
165
|
+
|
|
166
|
+
return decorator
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
hook_logger = get_logger("crackerjack.hooks")
|
|
170
|
+
test_logger = get_logger("crackerjack.tests")
|
|
171
|
+
config_logger = get_logger("crackerjack.config")
|
|
172
|
+
cache_logger = get_logger("crackerjack.cache")
|
|
173
|
+
security_logger = get_logger("crackerjack.security")
|
|
174
|
+
performance_logger = get_logger("crackerjack.performance")
|