x-ipe 1.0.24__py3-none-any.whl → 1.0.25__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.
- x_ipe/app.py +25 -3
- x_ipe/handlers/terminal_handlers.py +6 -0
- x_ipe/handlers/voice_handlers.py +5 -0
- x_ipe/resources/copilot-instructions.md +19 -6
- x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
- x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
- x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
- x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
- x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
- x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
- x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
- x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
- x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
- x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
- x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
- x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
- x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
- x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
- x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
- x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
- x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
- x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
- x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
- x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
- x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
- x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
- x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
- x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
- x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
- x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
- x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
- x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
- x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
- x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
- x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
- x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
- x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
- x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
- x_ipe/routes/__init__.py +2 -0
- x_ipe/routes/ideas_routes.py +17 -0
- x_ipe/routes/kb_routes.py +80 -0
- x_ipe/routes/main_routes.py +18 -0
- x_ipe/routes/project_routes.py +7 -0
- x_ipe/routes/proxy_routes.py +2 -0
- x_ipe/routes/quality_evaluation_routes.py +193 -0
- x_ipe/routes/settings_routes.py +6 -0
- x_ipe/routes/tools_routes.py +6 -0
- x_ipe/routes/tracing_routes.py +232 -0
- x_ipe/routes/uiux_feedback_routes.py +30 -0
- x_ipe/services/__init__.py +5 -0
- x_ipe/services/config_service.py +6 -0
- x_ipe/services/file_service.py +20 -0
- x_ipe/services/homepage_service.py +160 -0
- x_ipe/services/ideas_service.py +19 -0
- x_ipe/services/kb_service.py +378 -0
- x_ipe/services/proxy_service.py +4 -0
- x_ipe/services/settings_service.py +13 -0
- x_ipe/services/skills_service.py +4 -0
- x_ipe/services/terminal_service.py +24 -0
- x_ipe/services/themes_service.py +4 -0
- x_ipe/services/tools_config_service.py +4 -0
- x_ipe/services/tracing_service.py +333 -0
- x_ipe/services/uiux_feedback_service.py +32 -0
- x_ipe/services/voice_input_service_v2.py +11 -0
- x_ipe/static/css/base.css +7 -0
- x_ipe/static/css/homepage-infinity.css +330 -0
- x_ipe/static/css/kb-core.css +301 -0
- x_ipe/static/css/quality-evaluation.css +345 -0
- x_ipe/static/css/sidebar.css +14 -4
- x_ipe/static/css/terminal.css +1 -0
- x_ipe/static/css/tracing-dashboard.css +796 -0
- x_ipe/static/css/workplace.css +20 -0
- x_ipe/static/img/homepage-infinity-loop.png +0 -0
- x_ipe/static/js/features/homepage-infinity.js +314 -0
- x_ipe/static/js/features/kb-core.js +371 -0
- x_ipe/static/js/features/quality-evaluation.js +387 -0
- x_ipe/static/js/features/sidebar.js +255 -12
- x_ipe/static/js/features/tracing-dashboard.js +855 -0
- x_ipe/static/js/features/tracing-graph.js +1031 -0
- x_ipe/static/js/features/tree-search.js +6 -2
- x_ipe/static/js/features/workplace.js +200 -6
- x_ipe/static/js/init.js +76 -0
- x_ipe/static/js/uiux-feedback.js +18 -2
- x_ipe/templates/base.html +19 -0
- x_ipe/templates/index.html +7 -1
- x_ipe/templates/knowledge-base.html +110 -0
- x_ipe/templates/workplace.html +4 -0
- x_ipe/tracing/__init__.py +37 -0
- x_ipe/tracing/buffer.py +135 -0
- x_ipe/tracing/context.py +125 -0
- x_ipe/tracing/decorator.py +288 -0
- x_ipe/tracing/middleware.py +197 -0
- x_ipe/tracing/parser.py +235 -0
- x_ipe/tracing/redactor.py +111 -0
- x_ipe/tracing/writer.py +122 -0
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/RECORD +132 -62
- x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
- x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
- x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
- x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
- x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
- x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
- x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
- {x_ipe-1.0.24.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
x_ipe/tracing/buffer.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Core
|
|
3
|
+
|
|
4
|
+
TraceBuffer and TraceEntry for in-memory trace storage.
|
|
5
|
+
|
|
6
|
+
Traces are collected in-memory during request execution and flushed
|
|
7
|
+
to log files upon request completion.
|
|
8
|
+
"""
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import List, Optional, Any
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TraceEntry:
|
|
17
|
+
"""
|
|
18
|
+
Single trace log entry representing a function call event.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
timestamp: When the event occurred
|
|
22
|
+
trace_id: Unique identifier for the trace
|
|
23
|
+
level: Log level (INFO, DEBUG, ERROR)
|
|
24
|
+
direction: Arrow direction (→ for entry, ← for exit)
|
|
25
|
+
event_type: Type of event (start_function, return_function, exception)
|
|
26
|
+
function_name: Name of the traced function
|
|
27
|
+
data: Parameters, return value, or error details
|
|
28
|
+
duration_ms: Execution time in milliseconds (only for exit events)
|
|
29
|
+
depth: Nesting level (0 = root)
|
|
30
|
+
"""
|
|
31
|
+
timestamp: datetime
|
|
32
|
+
trace_id: str
|
|
33
|
+
level: str
|
|
34
|
+
direction: str
|
|
35
|
+
event_type: str
|
|
36
|
+
function_name: str
|
|
37
|
+
data: dict
|
|
38
|
+
duration_ms: Optional[float] = None
|
|
39
|
+
depth: int = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TraceBuffer:
|
|
43
|
+
"""
|
|
44
|
+
In-memory buffer for trace entries during request execution.
|
|
45
|
+
|
|
46
|
+
Collects trace entries and formats them for log file output.
|
|
47
|
+
Enforces a maximum size limit to prevent memory exhaustion.
|
|
48
|
+
|
|
49
|
+
Usage:
|
|
50
|
+
buffer = TraceBuffer("abc-123", "POST /api/orders")
|
|
51
|
+
buffer.add(TraceEntry(...))
|
|
52
|
+
log_content = buffer.to_log_string("SUCCESS", 150.0)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
MAX_SIZE = 10 * 1024 * 1024 # 10MB limit
|
|
56
|
+
|
|
57
|
+
def __init__(self, trace_id: str, root_api: str):
|
|
58
|
+
"""
|
|
59
|
+
Initialize TraceBuffer.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
trace_id: Unique identifier for this trace
|
|
63
|
+
root_api: The root API call (e.g., "POST /api/orders")
|
|
64
|
+
"""
|
|
65
|
+
self.trace_id = trace_id
|
|
66
|
+
self.root_api = root_api
|
|
67
|
+
self.started_at = datetime.now(timezone.utc)
|
|
68
|
+
self.entries: List[TraceEntry] = []
|
|
69
|
+
self._size = 0
|
|
70
|
+
|
|
71
|
+
def add(self, entry: TraceEntry) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Add a trace entry to the buffer.
|
|
74
|
+
|
|
75
|
+
Silently drops entries if buffer size limit is exceeded.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
entry: TraceEntry to add
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
entry_size = len(json.dumps(entry.data, default=str))
|
|
82
|
+
except (TypeError, ValueError):
|
|
83
|
+
entry_size = 100 # Fallback size estimate
|
|
84
|
+
|
|
85
|
+
if self._size + entry_size > self.MAX_SIZE:
|
|
86
|
+
return # Silently drop if buffer full
|
|
87
|
+
|
|
88
|
+
self.entries.append(entry)
|
|
89
|
+
self._size += entry_size
|
|
90
|
+
|
|
91
|
+
def to_log_string(self, status: str, total_ms: float) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Format the buffer as a log file string.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
status: Final status (SUCCESS, ERROR)
|
|
97
|
+
total_ms: Total execution time in milliseconds
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Formatted log string ready for file output
|
|
101
|
+
"""
|
|
102
|
+
lines = [
|
|
103
|
+
f"[TRACE-START] {self.trace_id} | {self.root_api} | {self.started_at.isoformat()}Z"
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for entry in self.entries:
|
|
107
|
+
indent = " " * (entry.depth + 1)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
data_str = json.dumps(entry.data, default=str, ensure_ascii=False)
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
data_str = str(entry.data)
|
|
113
|
+
|
|
114
|
+
# Truncate very long data
|
|
115
|
+
if len(data_str) > 1000:
|
|
116
|
+
data_str = data_str[:997] + "..."
|
|
117
|
+
|
|
118
|
+
if entry.duration_ms is not None:
|
|
119
|
+
line = (
|
|
120
|
+
f"{indent}[{entry.level}] {entry.direction} {entry.event_type}: "
|
|
121
|
+
f"{entry.function_name} | {data_str} | {entry.duration_ms:.0f}ms"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
line = (
|
|
125
|
+
f"{indent}[{entry.level}] {entry.direction} {entry.event_type}: "
|
|
126
|
+
f"{entry.function_name} | {data_str}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
lines.append(line)
|
|
130
|
+
|
|
131
|
+
lines.append(
|
|
132
|
+
f"[TRACE-END] {self.trace_id} | {total_ms:.0f}ms | {status}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return "\n".join(lines)
|
x_ipe/tracing/context.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Core
|
|
3
|
+
|
|
4
|
+
TraceContext for thread-safe context propagation using contextvars.
|
|
5
|
+
|
|
6
|
+
Manages the active trace context across nested function calls,
|
|
7
|
+
ensuring trace ID propagation and proper nesting depth tracking.
|
|
8
|
+
"""
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from typing import Optional, List
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
from .buffer import TraceBuffer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Thread-safe context variable for the active trace
|
|
17
|
+
_trace_context: ContextVar[Optional['TraceContext']] = ContextVar(
|
|
18
|
+
'trace_context',
|
|
19
|
+
default=None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TraceContext:
|
|
24
|
+
"""
|
|
25
|
+
Thread-safe trace context manager.
|
|
26
|
+
|
|
27
|
+
Uses Python's contextvars to maintain trace state across async
|
|
28
|
+
function calls. Tracks trace ID, buffer, and call depth.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
# Start a new trace
|
|
32
|
+
ctx = TraceContext.start_trace("POST /api/orders")
|
|
33
|
+
|
|
34
|
+
# Get current context (in nested calls)
|
|
35
|
+
ctx = TraceContext.get_current()
|
|
36
|
+
|
|
37
|
+
# End trace and get buffer
|
|
38
|
+
buffer = TraceContext.end_trace()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, trace_id: str, root_api: str):
|
|
42
|
+
"""
|
|
43
|
+
Initialize TraceContext.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
trace_id: Unique identifier for this trace
|
|
47
|
+
root_api: The root API call (e.g., "POST /api/orders")
|
|
48
|
+
"""
|
|
49
|
+
self.trace_id = trace_id
|
|
50
|
+
self.buffer = TraceBuffer(trace_id, root_api)
|
|
51
|
+
self.depth = 0
|
|
52
|
+
self._call_stack: List[str] = []
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def start_trace(cls, root_api: str) -> 'TraceContext':
|
|
56
|
+
"""
|
|
57
|
+
Start a new trace.
|
|
58
|
+
|
|
59
|
+
Generates a unique trace ID and creates a new context.
|
|
60
|
+
Sets the context as the active trace.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
root_api: The root API call (e.g., "POST /api/orders")
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
New TraceContext instance
|
|
67
|
+
"""
|
|
68
|
+
# Generate short UUID for readability (first 13 chars)
|
|
69
|
+
trace_id = str(uuid.uuid4())[:13]
|
|
70
|
+
ctx = cls(trace_id, root_api)
|
|
71
|
+
_trace_context.set(ctx)
|
|
72
|
+
return ctx
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_current(cls) -> Optional['TraceContext']:
|
|
76
|
+
"""
|
|
77
|
+
Get the current active trace context.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Active TraceContext or None if no trace is active
|
|
81
|
+
"""
|
|
82
|
+
return _trace_context.get()
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def end_trace(cls) -> Optional[TraceBuffer]:
|
|
86
|
+
"""
|
|
87
|
+
End the current trace and return the buffer.
|
|
88
|
+
|
|
89
|
+
Clears the active context.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
TraceBuffer containing all trace entries, or None if no active trace
|
|
93
|
+
"""
|
|
94
|
+
ctx = _trace_context.get()
|
|
95
|
+
if ctx:
|
|
96
|
+
_trace_context.set(None)
|
|
97
|
+
return ctx.buffer
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def push_call(self, func_name: str) -> int:
|
|
101
|
+
"""
|
|
102
|
+
Push a function call onto the stack.
|
|
103
|
+
|
|
104
|
+
Increments depth and tracks the function name.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
func_name: Name of the function being called
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Depth level before increment (for logging)
|
|
111
|
+
"""
|
|
112
|
+
self._call_stack.append(func_name)
|
|
113
|
+
depth = self.depth
|
|
114
|
+
self.depth += 1
|
|
115
|
+
return depth
|
|
116
|
+
|
|
117
|
+
def pop_call(self) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Pop a function call from the stack.
|
|
120
|
+
|
|
121
|
+
Decrements depth and removes the function from tracking.
|
|
122
|
+
"""
|
|
123
|
+
if self._call_stack:
|
|
124
|
+
self._call_stack.pop()
|
|
125
|
+
self.depth -= 1
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FEATURE-023: Application Action Tracing - Core
|
|
3
|
+
|
|
4
|
+
@x_ipe_tracing decorator for automatic function tracing.
|
|
5
|
+
|
|
6
|
+
Provides a decorator that automatically logs function entry, exit,
|
|
7
|
+
return values, and exceptions with execution timing.
|
|
8
|
+
"""
|
|
9
|
+
import functools
|
|
10
|
+
import time
|
|
11
|
+
import asyncio
|
|
12
|
+
import inspect
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from typing import Callable, List, Optional, Any, Dict
|
|
15
|
+
|
|
16
|
+
from .context import TraceContext
|
|
17
|
+
from .buffer import TraceEntry
|
|
18
|
+
from .redactor import Redactor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def x_ipe_tracing(
|
|
22
|
+
level: str = "INFO",
|
|
23
|
+
redact: Optional[List[str]] = None
|
|
24
|
+
) -> Callable:
|
|
25
|
+
"""
|
|
26
|
+
Decorator for automatic function tracing.
|
|
27
|
+
|
|
28
|
+
Logs function entry with parameters, exit with return value,
|
|
29
|
+
and any exceptions that occur. Automatically redacts sensitive data.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
level: Log level - "INFO", "DEBUG", or "SKIP"
|
|
33
|
+
redact: List of parameter names to redact (in addition to built-in patterns)
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
@x_ipe_tracing(level="INFO", redact=["password"])
|
|
37
|
+
def create_user(email: str, password: str) -> dict:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Decorated function that traces execution
|
|
42
|
+
"""
|
|
43
|
+
if level == "SKIP":
|
|
44
|
+
return lambda fn: fn # No-op decorator
|
|
45
|
+
|
|
46
|
+
redactor = Redactor(custom_fields=redact)
|
|
47
|
+
|
|
48
|
+
def decorator(func: Callable) -> Callable:
|
|
49
|
+
@functools.wraps(func)
|
|
50
|
+
def sync_wrapper(*args, **kwargs):
|
|
51
|
+
ctx = TraceContext.get_current()
|
|
52
|
+
if not ctx:
|
|
53
|
+
return func(*args, **kwargs) # No active trace
|
|
54
|
+
|
|
55
|
+
return _trace_call(ctx, func, args, kwargs, level, redactor)
|
|
56
|
+
|
|
57
|
+
@functools.wraps(func)
|
|
58
|
+
async def async_wrapper(*args, **kwargs):
|
|
59
|
+
ctx = TraceContext.get_current()
|
|
60
|
+
if not ctx:
|
|
61
|
+
return await func(*args, **kwargs) # No active trace
|
|
62
|
+
|
|
63
|
+
return await _trace_call_async(ctx, func, args, kwargs, level, redactor)
|
|
64
|
+
|
|
65
|
+
if asyncio.iscoroutinefunction(func):
|
|
66
|
+
return async_wrapper
|
|
67
|
+
return sync_wrapper
|
|
68
|
+
|
|
69
|
+
return decorator
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_params(func: Callable, args: tuple, kwargs: dict) -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Extract function parameters as a dictionary.
|
|
75
|
+
|
|
76
|
+
Uses function signature to map positional args to parameter names.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
func: The function being called
|
|
80
|
+
args: Positional arguments
|
|
81
|
+
kwargs: Keyword arguments
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Dictionary of parameter names to values
|
|
85
|
+
"""
|
|
86
|
+
params = {}
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
sig = inspect.signature(func)
|
|
90
|
+
param_names = list(sig.parameters.keys())
|
|
91
|
+
|
|
92
|
+
# Map positional args
|
|
93
|
+
for i, arg in enumerate(args):
|
|
94
|
+
if i < len(param_names):
|
|
95
|
+
# Skip 'self' and 'cls' parameters
|
|
96
|
+
param_name = param_names[i]
|
|
97
|
+
if param_name not in ('self', 'cls'):
|
|
98
|
+
params[param_name] = arg
|
|
99
|
+
else:
|
|
100
|
+
params[f"arg_{i}"] = arg
|
|
101
|
+
|
|
102
|
+
# Add keyword args
|
|
103
|
+
params.update(kwargs)
|
|
104
|
+
except (ValueError, TypeError):
|
|
105
|
+
# Fallback if signature introspection fails
|
|
106
|
+
for i, arg in enumerate(args):
|
|
107
|
+
params[f"arg_{i}"] = arg
|
|
108
|
+
params.update(kwargs)
|
|
109
|
+
|
|
110
|
+
return params
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _safe_serialize(value: Any) -> Any:
|
|
114
|
+
"""
|
|
115
|
+
Safely serialize a value for logging.
|
|
116
|
+
|
|
117
|
+
Handles circular references and non-serializable objects.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
value: Value to serialize
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Serializable representation of the value
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# Try direct serialization
|
|
127
|
+
import json
|
|
128
|
+
json.dumps(value, default=str)
|
|
129
|
+
return value
|
|
130
|
+
except (TypeError, ValueError, RecursionError):
|
|
131
|
+
# Fallback for complex objects
|
|
132
|
+
return str(value)[:500]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _trace_call(
|
|
136
|
+
ctx: TraceContext,
|
|
137
|
+
func: Callable,
|
|
138
|
+
args: tuple,
|
|
139
|
+
kwargs: dict,
|
|
140
|
+
level: str,
|
|
141
|
+
redactor: Redactor
|
|
142
|
+
) -> Any:
|
|
143
|
+
"""
|
|
144
|
+
Trace a synchronous function call.
|
|
145
|
+
|
|
146
|
+
Logs entry, executes the function, logs exit or exception.
|
|
147
|
+
"""
|
|
148
|
+
func_name = func.__name__
|
|
149
|
+
depth = ctx.push_call(func_name)
|
|
150
|
+
|
|
151
|
+
# Extract and redact parameters
|
|
152
|
+
params = _extract_params(func, args, kwargs)
|
|
153
|
+
redacted_params = redactor.redact(params)
|
|
154
|
+
|
|
155
|
+
# Log entry
|
|
156
|
+
ctx.buffer.add(TraceEntry(
|
|
157
|
+
timestamp=datetime.now(timezone.utc),
|
|
158
|
+
trace_id=ctx.trace_id,
|
|
159
|
+
level=level,
|
|
160
|
+
direction="→",
|
|
161
|
+
event_type="start_function",
|
|
162
|
+
function_name=func_name,
|
|
163
|
+
data=_safe_serialize(redacted_params),
|
|
164
|
+
depth=depth
|
|
165
|
+
))
|
|
166
|
+
|
|
167
|
+
start = time.perf_counter()
|
|
168
|
+
try:
|
|
169
|
+
result = func(*args, **kwargs)
|
|
170
|
+
duration = (time.perf_counter() - start) * 1000
|
|
171
|
+
|
|
172
|
+
# Redact and serialize return value
|
|
173
|
+
redacted_result = redactor.redact({"return": _safe_serialize(result)})
|
|
174
|
+
|
|
175
|
+
# Log success
|
|
176
|
+
ctx.buffer.add(TraceEntry(
|
|
177
|
+
timestamp=datetime.now(timezone.utc),
|
|
178
|
+
trace_id=ctx.trace_id,
|
|
179
|
+
level=level,
|
|
180
|
+
direction="←",
|
|
181
|
+
event_type="return_function",
|
|
182
|
+
function_name=func_name,
|
|
183
|
+
data=redacted_result,
|
|
184
|
+
duration_ms=duration,
|
|
185
|
+
depth=depth
|
|
186
|
+
))
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
duration = (time.perf_counter() - start) * 1000
|
|
191
|
+
|
|
192
|
+
# Log error
|
|
193
|
+
ctx.buffer.add(TraceEntry(
|
|
194
|
+
timestamp=datetime.now(timezone.utc),
|
|
195
|
+
trace_id=ctx.trace_id,
|
|
196
|
+
level="ERROR",
|
|
197
|
+
direction="←",
|
|
198
|
+
event_type="exception",
|
|
199
|
+
function_name=func_name,
|
|
200
|
+
data={
|
|
201
|
+
"error": type(e).__name__,
|
|
202
|
+
"message": str(e)
|
|
203
|
+
},
|
|
204
|
+
duration_ms=duration,
|
|
205
|
+
depth=depth
|
|
206
|
+
))
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
finally:
|
|
210
|
+
ctx.pop_call()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
async def _trace_call_async(
|
|
214
|
+
ctx: TraceContext,
|
|
215
|
+
func: Callable,
|
|
216
|
+
args: tuple,
|
|
217
|
+
kwargs: dict,
|
|
218
|
+
level: str,
|
|
219
|
+
redactor: Redactor
|
|
220
|
+
) -> Any:
|
|
221
|
+
"""
|
|
222
|
+
Trace an asynchronous function call.
|
|
223
|
+
|
|
224
|
+
Logs entry, awaits the function, logs exit or exception.
|
|
225
|
+
"""
|
|
226
|
+
func_name = func.__name__
|
|
227
|
+
depth = ctx.push_call(func_name)
|
|
228
|
+
|
|
229
|
+
# Extract and redact parameters
|
|
230
|
+
params = _extract_params(func, args, kwargs)
|
|
231
|
+
redacted_params = redactor.redact(params)
|
|
232
|
+
|
|
233
|
+
# Log entry
|
|
234
|
+
ctx.buffer.add(TraceEntry(
|
|
235
|
+
timestamp=datetime.now(timezone.utc),
|
|
236
|
+
trace_id=ctx.trace_id,
|
|
237
|
+
level=level,
|
|
238
|
+
direction="→",
|
|
239
|
+
event_type="start_function",
|
|
240
|
+
function_name=func_name,
|
|
241
|
+
data=_safe_serialize(redacted_params),
|
|
242
|
+
depth=depth
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
start = time.perf_counter()
|
|
246
|
+
try:
|
|
247
|
+
result = await func(*args, **kwargs)
|
|
248
|
+
duration = (time.perf_counter() - start) * 1000
|
|
249
|
+
|
|
250
|
+
# Redact and serialize return value
|
|
251
|
+
redacted_result = redactor.redact({"return": _safe_serialize(result)})
|
|
252
|
+
|
|
253
|
+
# Log success
|
|
254
|
+
ctx.buffer.add(TraceEntry(
|
|
255
|
+
timestamp=datetime.now(timezone.utc),
|
|
256
|
+
trace_id=ctx.trace_id,
|
|
257
|
+
level=level,
|
|
258
|
+
direction="←",
|
|
259
|
+
event_type="return_function",
|
|
260
|
+
function_name=func_name,
|
|
261
|
+
data=redacted_result,
|
|
262
|
+
duration_ms=duration,
|
|
263
|
+
depth=depth
|
|
264
|
+
))
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
duration = (time.perf_counter() - start) * 1000
|
|
269
|
+
|
|
270
|
+
# Log error
|
|
271
|
+
ctx.buffer.add(TraceEntry(
|
|
272
|
+
timestamp=datetime.now(timezone.utc),
|
|
273
|
+
trace_id=ctx.trace_id,
|
|
274
|
+
level="ERROR",
|
|
275
|
+
direction="←",
|
|
276
|
+
event_type="exception",
|
|
277
|
+
function_name=func_name,
|
|
278
|
+
data={
|
|
279
|
+
"error": type(e).__name__,
|
|
280
|
+
"message": str(e)
|
|
281
|
+
},
|
|
282
|
+
duration_ms=duration,
|
|
283
|
+
depth=depth
|
|
284
|
+
))
|
|
285
|
+
raise
|
|
286
|
+
|
|
287
|
+
finally:
|
|
288
|
+
ctx.pop_call()
|