crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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 +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -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 +657 -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 +409 -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 +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -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 +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -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 +372 -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 +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -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/coverage_improvement.py +223 -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 +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -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 +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -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.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.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.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.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")
|