severn-trace 0.1.0__tar.gz
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.
- severn_trace-0.1.0/PKG-INFO +3 -0
- severn_trace-0.1.0/README.md +1 -0
- severn_trace-0.1.0/pyproject.toml +14 -0
- severn_trace-0.1.0/setup.cfg +4 -0
- severn_trace-0.1.0/severn/__init__.py +17 -0
- severn_trace-0.1.0/severn/cli.py +243 -0
- severn_trace-0.1.0/severn/exporter.py +171 -0
- severn_trace-0.1.0/severn/tracer.py +433 -0
- severn_trace-0.1.0/severn_trace.egg-info/PKG-INFO +3 -0
- severn_trace-0.1.0/severn_trace.egg-info/SOURCES.txt +11 -0
- severn_trace-0.1.0/severn_trace.egg-info/dependency_links.txt +1 -0
- severn_trace-0.1.0/severn_trace.egg-info/entry_points.txt +2 -0
- severn_trace-0.1.0/severn_trace.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Severn Trace
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "severn-trace"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[tool.setuptools]
|
|
11
|
+
packages = ["severn"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
severn = "severn.cli:interactive_menu"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""severn-trace: lightweight execution tracing for autonomous AI agents."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .exporter import Exporter, configure, flush, get_exporter
|
|
6
|
+
from .tracer import FAILED, SUCCESS, trace
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"trace",
|
|
10
|
+
"configure",
|
|
11
|
+
"flush",
|
|
12
|
+
"get_exporter",
|
|
13
|
+
"Exporter",
|
|
14
|
+
"SUCCESS",
|
|
15
|
+
"FAILED",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
# Rich Cyberpunk & Terminal ANSI Color Palette
|
|
7
|
+
CYAN = "\033[96m"
|
|
8
|
+
GREEN = "\033[92m"
|
|
9
|
+
YELLOW = "\033[93m"
|
|
10
|
+
MAGENTA = "\033[95m"
|
|
11
|
+
BLUE = "\033[94m"
|
|
12
|
+
RED = "\033[91m"
|
|
13
|
+
WHITE = "\033[97m"
|
|
14
|
+
RESET = "\033[0m"
|
|
15
|
+
BOLD = "\033[1m"
|
|
16
|
+
DIM = "\033[2m"
|
|
17
|
+
ORANGE = "\033[38;5;208m"
|
|
18
|
+
PURPLE = "\033[38;5;141m"
|
|
19
|
+
TEAL = "\033[38;5;38m"
|
|
20
|
+
|
|
21
|
+
def show_banner():
|
|
22
|
+
os.system('clear' if os.name == 'posix' else 'cls')
|
|
23
|
+
|
|
24
|
+
# Clean 3D Block ASCII Banner for SEVERN
|
|
25
|
+
banner = f"""{CYAN}{BOLD}
|
|
26
|
+
███████╗███████╗██╗ ██╗███████╗██████╗ ███╗ ██╗
|
|
27
|
+
██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗████╗ ██║
|
|
28
|
+
███████╗█████╗ ██║ ██║█████╗ ██████╔╝██╔██╗ ██║
|
|
29
|
+
╚════██║██╔══╝ ╚██ ██╔╝██╔══╝ ██╔══██╗██║╚██╗██║
|
|
30
|
+
███████║███████╗ ╚████╔╝ ███████╗██║ ██║██║ ╚████║
|
|
31
|
+
╚══════╝╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝
|
|
32
|
+
{RESET}{PURPLE}=================================================================================={RESET}
|
|
33
|
+
{MAGENTA}{BOLD} ⚡ ZERO-DEPENDENCY EXECUTION RUNTIME & ERROR TELEMETRY LAYER FOR AI AGENTS ⚡{RESET}
|
|
34
|
+
{PURPLE}=================================================================================={RESET}
|
|
35
|
+
"""
|
|
36
|
+
print(banner)
|
|
37
|
+
|
|
38
|
+
def get_trace_stats():
|
|
39
|
+
"""Analyzes local log files to compute real-time metrics."""
|
|
40
|
+
log_path = "severn_traces.jsonl"
|
|
41
|
+
if not os.path.exists(log_path):
|
|
42
|
+
return 0, 0, 0
|
|
43
|
+
|
|
44
|
+
total = 0
|
|
45
|
+
passed = 0
|
|
46
|
+
failed = 0
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
with open(log_path, "r", encoding="utf-8") as f:
|
|
50
|
+
for line in f:
|
|
51
|
+
total += 1
|
|
52
|
+
if '"status": "SUCCESS"' in line or '"status":"SUCCESS"' in line:
|
|
53
|
+
passed += 1
|
|
54
|
+
elif '"status": "FAILED"' in line or '"status":"FAILED"' in line:
|
|
55
|
+
failed += 1
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return total, passed, failed
|
|
60
|
+
|
|
61
|
+
def pause_and_return():
|
|
62
|
+
input(f"\n{DIM}Press Enter to return...{RESET}")
|
|
63
|
+
|
|
64
|
+
# -------------------------------------------------------------------------
|
|
65
|
+
# MULTI-STEP AGENT FRAMEWORK ARCHITECTURE EXPLORER (LangChain / CrewAI / AutoGen)
|
|
66
|
+
# -------------------------------------------------------------------------
|
|
67
|
+
def sub_menu_agent_topology():
|
|
68
|
+
while True:
|
|
69
|
+
show_banner()
|
|
70
|
+
print(f"{TEAL}{BOLD}🤖 AI AGENT FRAMEWORK INTEGRATION & TOPOLOGY RUNTIME:{RESET}\n")
|
|
71
|
+
print(f" {GREEN}[1]{RESET} {BOLD}CrewAI Agent Goal & Task Trapping{RESET}")
|
|
72
|
+
print(f" {CYAN}[2]{RESET} {BOLD}LangChain AgentExecutor State Machine Tracing{RESET}")
|
|
73
|
+
print(f" {YELLOW}[3]{RESET} {BOLD}AutoGen Multi-Agent Conversational Routing{RESET}")
|
|
74
|
+
print(f" {MAGENTA}[4]{RESET} {BOLD}Custom Autonomous Loop Pattern (ReAct / Plan-and-Solve){RESET}")
|
|
75
|
+
print(f" {RED}[B]{RESET} {BOLD}Back to Main Menu{RESET}\n")
|
|
76
|
+
|
|
77
|
+
choice = input(f"{CYAN}{BOLD}Select framework workflow (1-4 or B): {RESET}").strip().upper()
|
|
78
|
+
|
|
79
|
+
if choice == "1":
|
|
80
|
+
print(f"\n{GREEN}[+] CrewAI Task Execution Telemetry:{RESET}")
|
|
81
|
+
print(f" Wrap sequential/hierarchical CrewAI tasks directly with {BOLD}@trace{RESET}.")
|
|
82
|
+
print(" Tracks crew role assignments, action output schemas, and tool-retry loops.")
|
|
83
|
+
pause_and_return()
|
|
84
|
+
elif choice == "2":
|
|
85
|
+
print(f"\n{CYAN}[+] LangChain AgentExecutor Hooking:{RESET}")
|
|
86
|
+
print(f" Decorate intermediate tool executions and custom LLMChain steps with {BOLD}@trace{RESET}.")
|
|
87
|
+
print(" Traps tool input parsing errors before state degradation occurs.")
|
|
88
|
+
pause_and_return()
|
|
89
|
+
elif choice == "3":
|
|
90
|
+
print(f"\n{YELLOW}[+] AutoGen Multi-Agent Conversation Tracing:{RESET}")
|
|
91
|
+
print(f" Monitor inter-agent messages between ConversableAgent instances.")
|
|
92
|
+
print(" Logs step duration, message payload sizes, and dead-end conversation loops.")
|
|
93
|
+
pause_and_return()
|
|
94
|
+
elif choice == "4":
|
|
95
|
+
print(f"\n{MAGENTA}[+] Custom ReAct Execution Loop:{RESET}")
|
|
96
|
+
print(f" Pattern: {BOLD}Thought -> Action -> Observation -> Final Answer{RESET}")
|
|
97
|
+
print(" Severn exports structured JSON metadata per step for instant replay analysis.")
|
|
98
|
+
pause_and_return()
|
|
99
|
+
elif choice == "B":
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
# -------------------------------------------------------------------------
|
|
103
|
+
# LABLAB.AI HACKATHON MULTI-STEP PORTAL
|
|
104
|
+
# -------------------------------------------------------------------------
|
|
105
|
+
def sub_menu_hackathon():
|
|
106
|
+
while True:
|
|
107
|
+
show_banner()
|
|
108
|
+
print(f"{YELLOW}{BOLD}🏆 LABLAB.AI HACKATHON BUILDER PORTAL (MULTI-STEP EXPLORER):{RESET}\n")
|
|
109
|
+
print(f" {GREEN}[1]{RESET} {BOLD}Challenge Focus & Multi-Agent Tracks{RESET}")
|
|
110
|
+
print(f" {CYAN}[2]{RESET} {BOLD}Participant Tooling & Sponsor Grants{RESET}")
|
|
111
|
+
print(f" {MAGENTA}[3]{RESET} {BOLD}Telemetry Submission Guidelines{RESET}")
|
|
112
|
+
print(f" {RED}[B]{RESET} {BOLD}Back to Main Menu{RESET}\n")
|
|
113
|
+
|
|
114
|
+
choice = input(f"{CYAN}{BOLD}Select hackathon section (1-3 or B): {RESET}").strip().upper()
|
|
115
|
+
|
|
116
|
+
if choice == "1":
|
|
117
|
+
print(f"\n{GREEN}[+] Challenge Focus:{RESET}")
|
|
118
|
+
print(" Architect multi-step AI agents, trap tool-call failure loops,")
|
|
119
|
+
print(" and provide zero-friction local telemetry using Severn-Trace.")
|
|
120
|
+
pause_and_return()
|
|
121
|
+
elif choice == "2":
|
|
122
|
+
print(f"\n{CYAN}[+] Participant Perks:{RESET}")
|
|
123
|
+
print(" Access sponsor API credits, developer tooling grants, priority community channels,")
|
|
124
|
+
print(" and fast-track investor introductions for top projects.")
|
|
125
|
+
pause_and_return()
|
|
126
|
+
elif choice == "3":
|
|
127
|
+
print(f"\n{MAGENTA}[+] Submission Setup:{RESET}")
|
|
128
|
+
print(" 1. Enroll on Lablab.ai")
|
|
129
|
+
print(" 2. Import Severn: `from severn import trace`")
|
|
130
|
+
print(" 3. Decorate tool calls with `@trace` & include `severn_traces.jsonl` in your repo.")
|
|
131
|
+
pause_and_return()
|
|
132
|
+
elif choice == "B":
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# -------------------------------------------------------------------------
|
|
136
|
+
# ROADMAP & ADVANCED FEATURES MULTI-STEP
|
|
137
|
+
# -------------------------------------------------------------------------
|
|
138
|
+
def sub_menu_premium():
|
|
139
|
+
while True:
|
|
140
|
+
show_banner()
|
|
141
|
+
print(f"{MAGENTA}{BOLD}👑 ADVANCED ROADMAP & ENGINE SPECS (MULTI-STEP EXPLORER):{RESET}\n")
|
|
142
|
+
print(f" {GREEN}[1]{RESET} {BOLD}Automated Self-Healing Retry Loop{RESET}")
|
|
143
|
+
print(f" {CYAN}[2]{RESET} {BOLD}Zero-Dependency Telemetry Exporter{RESET}")
|
|
144
|
+
print(f" {YELLOW}[3]{RESET} {BOLD}Founding Builder Foundation Access{RESET}")
|
|
145
|
+
print(f" {RED}[B]{RESET} {BOLD}Back to Main Menu{RESET}\n")
|
|
146
|
+
|
|
147
|
+
choice = input(f"{CYAN}{BOLD}Select option (1-3 or B): {RESET}").strip().upper()
|
|
148
|
+
|
|
149
|
+
if choice == "1":
|
|
150
|
+
print(f"\n{GREEN}[+] Self-Healing Retries:{RESET}")
|
|
151
|
+
print(" Upcoming SDK releases will feature automatic retry loops for broken API calls,")
|
|
152
|
+
print(" sanitizing invalid JSON schemas before agent execution fails.")
|
|
153
|
+
pause_and_return()
|
|
154
|
+
elif choice == "2":
|
|
155
|
+
print(f"\n{CYAN}[+] Zero-Dependency Core:{RESET}")
|
|
156
|
+
print(" Built strictly with standard Python modules (json, functools, time, uuid).")
|
|
157
|
+
print(" Zero external library conflicts across Python 3.8+ environments.")
|
|
158
|
+
pause_and_return()
|
|
159
|
+
elif choice == "3":
|
|
160
|
+
print(f"\n{YELLOW}[+] Founding Builder Foundation:{RESET}")
|
|
161
|
+
print(" Early competition participants and top builders receive priority roadmap voting")
|
|
162
|
+
print(" and early access to enterprise telemetry scaling tools.")
|
|
163
|
+
pause_and_return()
|
|
164
|
+
elif choice == "B":
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
# -------------------------------------------------------------------------
|
|
168
|
+
# MAIN INTERACTIVE MENU LOOP
|
|
169
|
+
# -------------------------------------------------------------------------
|
|
170
|
+
def interactive_menu():
|
|
171
|
+
while True:
|
|
172
|
+
show_banner()
|
|
173
|
+
|
|
174
|
+
total, passed, failed = get_trace_stats()
|
|
175
|
+
|
|
176
|
+
print(f"{WHITE}{BOLD}📊 PUBLIC LIVE SESSION DASHBOARD (LOCAL TELEMETRY):{RESET}")
|
|
177
|
+
print(f" {CYAN}Total Local Telemetry Records:{RESET} {WHITE}{total}{RESET}")
|
|
178
|
+
print(f" {GREEN}Successfully Executed Steps: {RESET} {WHITE}{passed}{RESET}")
|
|
179
|
+
print(f" {RED}Caught Agent Loop Failures: {RESET} {WHITE}{failed}{RESET}\n")
|
|
180
|
+
|
|
181
|
+
print(f"{YELLOW}{BOLD}SELECT AN OPTION TO NAVIGATE SEVERN (1 - 7):{RESET}\n")
|
|
182
|
+
print(f" {GREEN}[1]{RESET} {BOLD}Verify Active Trace Status{RESET} - Check if local runtime engine is active")
|
|
183
|
+
print(f" {CYAN}[2]{RESET} {BOLD}View Local Telemetry Database{RESET} - Inspect raw `severn_traces.jsonl` export records")
|
|
184
|
+
print(f" {TEAL}[3]{RESET} {BOLD}Agent Framework Topologies{RESET} - Explore CrewAI, LangChain & AutoGen integration options")
|
|
185
|
+
print(f" {YELLOW}[4]{RESET} {BOLD}Lablab.ai Hackathon Portal{RESET} - Access multi-step AI Agent competition guides")
|
|
186
|
+
print(f" {MAGENTA}[5]{RESET} {BOLD}Claim Premium SDK Access Perks{RESET} - Explore future self-healing & framework features")
|
|
187
|
+
print(f" {BLUE}[6]{RESET} {BOLD}Direct Founder & Community Support{RESET} - Connect via Discord (@severnteam)")
|
|
188
|
+
print(f" {RED}[7]{RESET} {BOLD}Exit Severn Console{RESET}\n")
|
|
189
|
+
|
|
190
|
+
choice = input(f"{CYAN}{BOLD}Enter your choice (1-7): {RESET}").strip()
|
|
191
|
+
|
|
192
|
+
if choice == "1":
|
|
193
|
+
print(f"\n{GREEN}[+] Status: Severn Tracing Engine v0.1.0 is fully active and listening.{RESET}")
|
|
194
|
+
print(f" Instructions: Place {BOLD}@trace{RESET} on top of any Python function or agent tool call to auto-capture telemetry.")
|
|
195
|
+
pause_and_return()
|
|
196
|
+
|
|
197
|
+
elif choice == "2":
|
|
198
|
+
print(f"\n{YELLOW}[*] Reading local trace logs from severn_traces.jsonl...{RESET}")
|
|
199
|
+
log_path = "severn_traces.jsonl"
|
|
200
|
+
if os.path.exists(log_path):
|
|
201
|
+
with open(log_path, "r", encoding="utf-8") as f:
|
|
202
|
+
lines = f.readlines()
|
|
203
|
+
print(f"{GREEN}Found {len(lines)} total trace records stored locally on your machine:{RESET}\n")
|
|
204
|
+
for line in lines[-5:]:
|
|
205
|
+
print(f" {CYAN}→{RESET} {line.strip()}")
|
|
206
|
+
else:
|
|
207
|
+
print(f"{YELLOW}[!] No local trace records found yet. Execute a test agent script decorated with @trace first!{RESET}")
|
|
208
|
+
pause_and_return()
|
|
209
|
+
|
|
210
|
+
elif choice == "3":
|
|
211
|
+
sub_menu_agent_topology()
|
|
212
|
+
|
|
213
|
+
elif choice == "4":
|
|
214
|
+
sub_menu_hackathon()
|
|
215
|
+
|
|
216
|
+
elif choice == "5":
|
|
217
|
+
sub_menu_premium()
|
|
218
|
+
|
|
219
|
+
elif choice == "6":
|
|
220
|
+
blue_discord_art = f"""{BLUE}
|
|
221
|
+
______
|
|
222
|
+
.-' '-.
|
|
223
|
+
/ \\
|
|
224
|
+
| () () |
|
|
225
|
+
| __ |
|
|
226
|
+
\\ /
|
|
227
|
+
'-.______.-'
|
|
228
|
+
{RESET}"""
|
|
229
|
+
print(blue_discord_art)
|
|
230
|
+
print(f"{ORANGE}[💬] DIRECT FOUNDER & COMMUNITY SUPPORT:{RESET}")
|
|
231
|
+
print(f" Have questions, need help configuring your trace setup, or want to report agent failure stress tests?")
|
|
232
|
+
print(f" Reach out directly through our community channel:")
|
|
233
|
+
print(f" - Discord Community Handle: {BOLD}@severnteam{RESET}")
|
|
234
|
+
print(f" - Direct Founder Email Support: {BOLD}david@severn.dev{RESET}")
|
|
235
|
+
print(f" - Mission Statement: Built by a 15-year-old solo founder to solve autonomous agent reliability forever.")
|
|
236
|
+
pause_and_return()
|
|
237
|
+
|
|
238
|
+
elif choice == "7":
|
|
239
|
+
print(f"\n{YELLOW}Exiting Severn Console. Keep building exceptional autonomous systems!{RESET}\n")
|
|
240
|
+
sys.exit(0)
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
interactive_menu()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import queue
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import urllib.request
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
|
|
14
|
+
ENV_ENDPOINT = "SEVERN_ENDPOINT"
|
|
15
|
+
ENV_API_KEY = "SEVERN_API_KEY"
|
|
16
|
+
ENV_TIMEOUT = "SEVERN_TIMEOUT"
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT = 2.0
|
|
19
|
+
DEFAULT_QUEUE_SIZE = 1000
|
|
20
|
+
DEFAULT_FLUSH_TIMEOUT = 2.0
|
|
21
|
+
LOCAL_LOG_FILE = "severn_traces.jsonl"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Exporter:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
endpoint: Optional[str] = None,
|
|
28
|
+
timeout: Optional[float] = None,
|
|
29
|
+
api_key: Optional[str] = None,
|
|
30
|
+
max_queue: int = DEFAULT_QUEUE_SIZE,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._endpoint_override = endpoint
|
|
33
|
+
self._timeout_override = timeout
|
|
34
|
+
self._api_key_override = api_key
|
|
35
|
+
self._queue: "queue.Queue[Dict[str, Any]]" = queue.Queue(maxsize=max_queue)
|
|
36
|
+
self._thread: Optional[threading.Thread] = None
|
|
37
|
+
self._lock = threading.Lock()
|
|
38
|
+
self.dropped = 0
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def endpoint(self) -> Optional[str]:
|
|
42
|
+
value = self._endpoint_override or os.environ.get(ENV_ENDPOINT, "")
|
|
43
|
+
value = (value or "").strip()
|
|
44
|
+
if value.lower().startswith(("http://", "https://")):
|
|
45
|
+
return value
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def timeout(self) -> float:
|
|
50
|
+
if self._timeout_override is not None:
|
|
51
|
+
candidate: Any = self._timeout_override
|
|
52
|
+
else:
|
|
53
|
+
candidate = os.environ.get(ENV_TIMEOUT, "")
|
|
54
|
+
try:
|
|
55
|
+
value = float(candidate)
|
|
56
|
+
except (TypeError, ValueError):
|
|
57
|
+
return DEFAULT_TIMEOUT
|
|
58
|
+
return value if value > 0 else DEFAULT_TIMEOUT
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def api_key(self) -> Optional[str]:
|
|
62
|
+
return self._api_key_override or os.environ.get(ENV_API_KEY) or None
|
|
63
|
+
|
|
64
|
+
def export(self, payload: Dict[str, Any]) -> bool:
|
|
65
|
+
try:
|
|
66
|
+
with open(LOCAL_LOG_FILE, "a", encoding="utf-8") as f:
|
|
67
|
+
f.write(json.dumps(payload, default=str) + "\n")
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
if self.endpoint is None:
|
|
73
|
+
return False
|
|
74
|
+
self._ensure_thread()
|
|
75
|
+
self._queue.put_nowait(payload)
|
|
76
|
+
return True
|
|
77
|
+
except queue.Full:
|
|
78
|
+
self.dropped += 1
|
|
79
|
+
return False
|
|
80
|
+
except Exception:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
def flush(self, timeout: float = DEFAULT_FLUSH_TIMEOUT) -> bool:
|
|
84
|
+
deadline = time.monotonic() + max(timeout, 0.0)
|
|
85
|
+
while self._queue.unfinished_tasks > 0:
|
|
86
|
+
if time.monotonic() >= deadline:
|
|
87
|
+
return False
|
|
88
|
+
time.sleep(0.01)
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
def _ensure_thread(self) -> None:
|
|
92
|
+
thread = self._thread
|
|
93
|
+
if thread is not None and thread.is_alive():
|
|
94
|
+
return
|
|
95
|
+
with self._lock:
|
|
96
|
+
thread = self._thread
|
|
97
|
+
if thread is not None and thread.is_alive():
|
|
98
|
+
return
|
|
99
|
+
thread = threading.Thread(
|
|
100
|
+
target=self._run, name="severn-exporter", daemon=True
|
|
101
|
+
)
|
|
102
|
+
thread.start()
|
|
103
|
+
self._thread = thread
|
|
104
|
+
|
|
105
|
+
def _run(self) -> None:
|
|
106
|
+
while True:
|
|
107
|
+
payload = self._queue.get()
|
|
108
|
+
try:
|
|
109
|
+
self._send(payload)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
finally:
|
|
113
|
+
self._queue.task_done()
|
|
114
|
+
|
|
115
|
+
def _send(self, payload: Dict[str, Any]) -> None:
|
|
116
|
+
endpoint = self.endpoint
|
|
117
|
+
if endpoint is None:
|
|
118
|
+
return
|
|
119
|
+
body = json.dumps(payload, default=str).encode("utf-8")
|
|
120
|
+
headers = {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
"User-Agent": "severn-trace/" + __version__,
|
|
123
|
+
}
|
|
124
|
+
api_key = self.api_key
|
|
125
|
+
if api_key:
|
|
126
|
+
headers["Authorization"] = "Bearer " + api_key
|
|
127
|
+
request = urllib.request.Request(
|
|
128
|
+
endpoint, data=body, headers=headers, method="POST"
|
|
129
|
+
)
|
|
130
|
+
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
|
131
|
+
response.read(1024)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
_default_exporter = Exporter()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def get_exporter() -> Exporter:
|
|
138
|
+
return _default_exporter
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def configure(
|
|
142
|
+
endpoint: Optional[str] = None,
|
|
143
|
+
timeout: Optional[float] = None,
|
|
144
|
+
api_key: Optional[str] = None,
|
|
145
|
+
) -> Exporter:
|
|
146
|
+
exporter = _default_exporter
|
|
147
|
+
if endpoint is not None:
|
|
148
|
+
exporter._endpoint_override = endpoint
|
|
149
|
+
if timeout is not None:
|
|
150
|
+
exporter._timeout_override = timeout
|
|
151
|
+
if api_key is not None:
|
|
152
|
+
exporter._api_key_override = api_key
|
|
153
|
+
return exporter
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def export(payload: Dict[str, Any]) -> bool:
|
|
157
|
+
return _default_exporter.export(payload)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def flush(timeout: float = DEFAULT_FLUSH_TIMEOUT) -> bool:
|
|
161
|
+
return _default_exporter.flush(timeout)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _flush_at_exit() -> None:
|
|
165
|
+
try:
|
|
166
|
+
_default_exporter.flush(DEFAULT_FLUSH_TIMEOUT)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
atexit.register(_flush_at_exit)
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import inspect
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import traceback
|
|
9
|
+
import uuid
|
|
10
|
+
from contextvars import ContextVar
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from typing import Any, Callable, Dict, Iterable, Optional, TypeVar, Union
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from . import exporter as _exporter
|
|
16
|
+
|
|
17
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
18
|
+
|
|
19
|
+
SUCCESS = "SUCCESS"
|
|
20
|
+
FAILED = "FAILED"
|
|
21
|
+
|
|
22
|
+
_SENSITIVE_FRAGMENTS = (
|
|
23
|
+
"password",
|
|
24
|
+
"passwd",
|
|
25
|
+
"secret",
|
|
26
|
+
"token",
|
|
27
|
+
"api_key",
|
|
28
|
+
"apikey",
|
|
29
|
+
"authorization",
|
|
30
|
+
"credential",
|
|
31
|
+
"private_key",
|
|
32
|
+
)
|
|
33
|
+
_REDACTED = "***REDACTED***"
|
|
34
|
+
_TERMINAL_PREVIEW_CHARS = 80
|
|
35
|
+
|
|
36
|
+
_RESET = "\033[0m"
|
|
37
|
+
_BOLD = "\033[1m"
|
|
38
|
+
_DIM = "\033[2m"
|
|
39
|
+
_RED = "\033[31m"
|
|
40
|
+
_GREEN = "\033[32m"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _SpanContext:
|
|
44
|
+
__slots__ = ("trace_id", "span_id", "depth")
|
|
45
|
+
|
|
46
|
+
def __init__(self, trace_id: str, span_id: str, depth: int) -> None:
|
|
47
|
+
self.trace_id = trace_id
|
|
48
|
+
self.span_id = span_id
|
|
49
|
+
self.depth = depth
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_current_span: "ContextVar[Optional[_SpanContext]]" = ContextVar(
|
|
53
|
+
"severn_current_span", default=None
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _preview(value: Any, limit: int) -> str:
|
|
58
|
+
try:
|
|
59
|
+
text = repr(value)
|
|
60
|
+
except Exception:
|
|
61
|
+
text = "<unrepresentable %s>" % type(value).__name__
|
|
62
|
+
text = text.replace("\r", " ").replace("\n", " ")
|
|
63
|
+
if len(text) > limit:
|
|
64
|
+
extra = len(text) - limit
|
|
65
|
+
text = text[:limit] + "...(+%d chars)" % extra
|
|
66
|
+
return text
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_sensitive(name: str, extra: frozenset) -> bool:
|
|
70
|
+
lowered = name.lower()
|
|
71
|
+
if lowered in extra:
|
|
72
|
+
return True
|
|
73
|
+
return any(fragment in lowered for fragment in _SENSITIVE_FRAGMENTS)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _capture_args(
|
|
77
|
+
signature: Optional[inspect.Signature],
|
|
78
|
+
args: tuple,
|
|
79
|
+
kwargs: dict,
|
|
80
|
+
limit: int,
|
|
81
|
+
redact: frozenset,
|
|
82
|
+
) -> Dict[str, str]:
|
|
83
|
+
captured: Dict[str, str] = {}
|
|
84
|
+
|
|
85
|
+
def put(name: str, value: Any) -> None:
|
|
86
|
+
if _is_sensitive(name, redact):
|
|
87
|
+
captured[name] = _REDACTED
|
|
88
|
+
else:
|
|
89
|
+
captured[name] = _preview(value, limit)
|
|
90
|
+
|
|
91
|
+
bound = None
|
|
92
|
+
if signature is not None:
|
|
93
|
+
try:
|
|
94
|
+
bound = signature.bind_partial(*args, **kwargs)
|
|
95
|
+
except TypeError:
|
|
96
|
+
bound = None
|
|
97
|
+
|
|
98
|
+
if bound is None:
|
|
99
|
+
for index, value in enumerate(args):
|
|
100
|
+
put("arg%d" % index, value)
|
|
101
|
+
for key, value in kwargs.items():
|
|
102
|
+
put(str(key), value)
|
|
103
|
+
return captured
|
|
104
|
+
|
|
105
|
+
for name, value in bound.arguments.items():
|
|
106
|
+
kind = signature.parameters[name].kind # type: ignore[union-attr]
|
|
107
|
+
if kind is inspect.Parameter.VAR_KEYWORD and isinstance(value, dict):
|
|
108
|
+
for key, item in value.items():
|
|
109
|
+
put(str(key), item)
|
|
110
|
+
elif name in ("self", "cls"):
|
|
111
|
+
continue
|
|
112
|
+
else:
|
|
113
|
+
put(name, value)
|
|
114
|
+
return captured
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _format_duration(ms: float) -> str:
|
|
118
|
+
if ms < 1000:
|
|
119
|
+
return "%.1fms" % ms
|
|
120
|
+
return "%.2fs" % (ms / 1000.0)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _error_type_name(exc: BaseException) -> str:
|
|
124
|
+
cls = type(exc)
|
|
125
|
+
if cls.__module__ in ("builtins", "__main__"):
|
|
126
|
+
return cls.__qualname__
|
|
127
|
+
return "%s.%s" % (cls.__module__, cls.__qualname__)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _color_enabled(stream: Any) -> bool:
|
|
131
|
+
if os.environ.get("NO_COLOR"):
|
|
132
|
+
return False
|
|
133
|
+
if os.environ.get("FORCE_COLOR"):
|
|
134
|
+
return True
|
|
135
|
+
try:
|
|
136
|
+
return bool(stream.isatty())
|
|
137
|
+
except Exception:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _symbols(stream: Any):
|
|
142
|
+
encoding = getattr(stream, "encoding", None) or "ascii"
|
|
143
|
+
try:
|
|
144
|
+
"✔✘→".encode(encoding)
|
|
145
|
+
return "✔", "✘", "→"
|
|
146
|
+
except Exception:
|
|
147
|
+
return "OK", "FAIL", "->"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _quiet(override: Optional[bool]) -> bool:
|
|
151
|
+
if override is not None:
|
|
152
|
+
return override
|
|
153
|
+
return os.environ.get("SEVERN_QUIET", "").strip().lower() in (
|
|
154
|
+
"1",
|
|
155
|
+
"true",
|
|
156
|
+
"yes",
|
|
157
|
+
"on",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _Config:
|
|
162
|
+
__slots__ = (
|
|
163
|
+
"step",
|
|
164
|
+
"signature",
|
|
165
|
+
"capture_args",
|
|
166
|
+
"capture_output",
|
|
167
|
+
"max_preview",
|
|
168
|
+
"quiet",
|
|
169
|
+
"show_traceback",
|
|
170
|
+
"redact",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
step: str,
|
|
176
|
+
signature: Optional[inspect.Signature],
|
|
177
|
+
capture_args: bool,
|
|
178
|
+
capture_output: bool,
|
|
179
|
+
max_preview: int,
|
|
180
|
+
quiet: Optional[bool],
|
|
181
|
+
show_traceback: bool,
|
|
182
|
+
redact: frozenset,
|
|
183
|
+
) -> None:
|
|
184
|
+
self.step = step
|
|
185
|
+
self.signature = signature
|
|
186
|
+
self.capture_args = capture_args
|
|
187
|
+
self.capture_output = capture_output
|
|
188
|
+
self.max_preview = max_preview
|
|
189
|
+
self.quiet = quiet
|
|
190
|
+
self.show_traceback = show_traceback
|
|
191
|
+
self.redact = redact
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _Span:
|
|
195
|
+
def __init__(self, config: _Config, args: tuple, kwargs: dict) -> None:
|
|
196
|
+
self.config = config
|
|
197
|
+
self.args_snapshot: Dict[str, str] = {}
|
|
198
|
+
self.trace_id = ""
|
|
199
|
+
self.span_id = ""
|
|
200
|
+
self.parent_span_id: Optional[str] = None
|
|
201
|
+
self.depth = 0
|
|
202
|
+
self.timestamp = ""
|
|
203
|
+
self._token: Any = None
|
|
204
|
+
self._t0 = time.perf_counter()
|
|
205
|
+
try:
|
|
206
|
+
if config.capture_args:
|
|
207
|
+
self.args_snapshot = _capture_args(
|
|
208
|
+
config.signature, args, kwargs, config.max_preview, config.redact
|
|
209
|
+
)
|
|
210
|
+
parent = _current_span.get()
|
|
211
|
+
self.trace_id = parent.trace_id if parent else uuid.uuid4().hex
|
|
212
|
+
self.span_id = uuid.uuid4().hex[:16]
|
|
213
|
+
self.parent_span_id = parent.span_id if parent else None
|
|
214
|
+
self.depth = parent.depth + 1 if parent else 0
|
|
215
|
+
self.timestamp = datetime.now(timezone.utc).isoformat(
|
|
216
|
+
timespec="milliseconds"
|
|
217
|
+
)
|
|
218
|
+
self._token = _current_span.set(
|
|
219
|
+
_SpanContext(self.trace_id, self.span_id, self.depth)
|
|
220
|
+
)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
self._t0 = time.perf_counter()
|
|
224
|
+
|
|
225
|
+
def succeed(self, result: Any) -> None:
|
|
226
|
+
latency_ms = (time.perf_counter() - self._t0) * 1000.0
|
|
227
|
+
self._finish(SUCCESS, latency_ms, result=result, exc=None)
|
|
228
|
+
|
|
229
|
+
def fail(self, exc: BaseException) -> None:
|
|
230
|
+
latency_ms = (time.perf_counter() - self._t0) * 1000.0
|
|
231
|
+
self._finish(FAILED, latency_ms, result=None, exc=exc)
|
|
232
|
+
|
|
233
|
+
def _finish(
|
|
234
|
+
self,
|
|
235
|
+
status: str,
|
|
236
|
+
latency_ms: float,
|
|
237
|
+
result: Any,
|
|
238
|
+
exc: Optional[BaseException],
|
|
239
|
+
) -> None:
|
|
240
|
+
try:
|
|
241
|
+
if self._token is not None:
|
|
242
|
+
try:
|
|
243
|
+
_current_span.reset(self._token)
|
|
244
|
+
except ValueError:
|
|
245
|
+
pass
|
|
246
|
+
self._token = None
|
|
247
|
+
|
|
248
|
+
config = self.config
|
|
249
|
+
output_preview: Optional[str] = None
|
|
250
|
+
error: Optional[Dict[str, str]] = None
|
|
251
|
+
|
|
252
|
+
if exc is None:
|
|
253
|
+
if config.capture_output:
|
|
254
|
+
output_preview = _preview(result, config.max_preview)
|
|
255
|
+
else:
|
|
256
|
+
tb = exc.__traceback__
|
|
257
|
+
inner = tb.tb_next if tb is not None and tb.tb_next is not None else tb
|
|
258
|
+
try:
|
|
259
|
+
message = str(exc)
|
|
260
|
+
except Exception:
|
|
261
|
+
message = "<unprintable %s>" % type(exc).__name__
|
|
262
|
+
error = {
|
|
263
|
+
"type": _error_type_name(exc),
|
|
264
|
+
"message": message,
|
|
265
|
+
"stacktrace": "".join(
|
|
266
|
+
traceback.format_exception(type(exc), exc, inner)
|
|
267
|
+
),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
payload: Dict[str, Any] = {
|
|
271
|
+
"sdk": "severn-trace",
|
|
272
|
+
"sdk_version": __version__,
|
|
273
|
+
"trace_id": self.trace_id,
|
|
274
|
+
"span_id": self.span_id,
|
|
275
|
+
"parent_span_id": self.parent_span_id,
|
|
276
|
+
"step": config.step,
|
|
277
|
+
"status": status,
|
|
278
|
+
"timestamp": self.timestamp,
|
|
279
|
+
"latency_ms": round(latency_ms, 3),
|
|
280
|
+
"args": self.args_snapshot if config.capture_args else None,
|
|
281
|
+
"output_preview": output_preview,
|
|
282
|
+
"error": error,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
self._print(status, latency_ms, output_preview, error, exc)
|
|
286
|
+
_exporter.export(payload)
|
|
287
|
+
except Exception:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
def _print(
|
|
291
|
+
self,
|
|
292
|
+
status: str,
|
|
293
|
+
latency_ms: float,
|
|
294
|
+
output_preview: Optional[str],
|
|
295
|
+
error: Optional[Dict[str, str]],
|
|
296
|
+
exc: Optional[BaseException],
|
|
297
|
+
) -> None:
|
|
298
|
+
config = self.config
|
|
299
|
+
if _quiet(config.quiet):
|
|
300
|
+
return
|
|
301
|
+
stream = sys.stderr
|
|
302
|
+
use_color = _color_enabled(stream)
|
|
303
|
+
ok_sym, fail_sym, arrow = _symbols(stream)
|
|
304
|
+
|
|
305
|
+
def paint(code: str, text: str) -> str:
|
|
306
|
+
return code + text + _RESET if use_color else text
|
|
307
|
+
|
|
308
|
+
indent = " " * self.depth
|
|
309
|
+
duration = _format_duration(latency_ms)
|
|
310
|
+
lines = []
|
|
311
|
+
|
|
312
|
+
if status == SUCCESS:
|
|
313
|
+
head = "%s%s %s %s" % (
|
|
314
|
+
indent,
|
|
315
|
+
paint(_GREEN, ok_sym),
|
|
316
|
+
paint(_BOLD, config.step),
|
|
317
|
+
paint(_DIM, duration),
|
|
318
|
+
)
|
|
319
|
+
if output_preview is not None:
|
|
320
|
+
head += " %s %s" % (
|
|
321
|
+
paint(_DIM, arrow),
|
|
322
|
+
paint(_DIM, _truncate(output_preview, _TERMINAL_PREVIEW_CHARS)),
|
|
323
|
+
)
|
|
324
|
+
lines.append(head)
|
|
325
|
+
else:
|
|
326
|
+
assert error is not None
|
|
327
|
+
lines.append(
|
|
328
|
+
"%s%s %s %s %s %s: %s"
|
|
329
|
+
% (
|
|
330
|
+
indent,
|
|
331
|
+
paint(_RED, fail_sym),
|
|
332
|
+
paint(_BOLD, config.step),
|
|
333
|
+
paint(_RED, "FAILED"),
|
|
334
|
+
paint(_DIM, duration),
|
|
335
|
+
paint(_RED, error["type"]),
|
|
336
|
+
_truncate(error["message"].replace("\n", " "), 200),
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
location = _innermost_location(exc)
|
|
340
|
+
if location:
|
|
341
|
+
lines.append("%s %s" % (indent, paint(_DIM, "at " + location)))
|
|
342
|
+
if config.show_traceback:
|
|
343
|
+
for raw in error["stacktrace"].rstrip().splitlines():
|
|
344
|
+
lines.append("%s %s" % (indent, paint(_DIM, raw)))
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
stream.write("\n".join(lines) + "\n")
|
|
348
|
+
stream.flush()
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _truncate(text: str, limit: int) -> str:
|
|
354
|
+
if len(text) <= limit:
|
|
355
|
+
return text
|
|
356
|
+
return text[: max(limit - 3, 0)] + "..."
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _innermost_location(exc: Optional[BaseException]) -> Optional[str]:
|
|
360
|
+
if exc is None or exc.__traceback__ is None:
|
|
361
|
+
return None
|
|
362
|
+
try:
|
|
363
|
+
frames = traceback.extract_tb(exc.__traceback__)
|
|
364
|
+
if not frames:
|
|
365
|
+
return None
|
|
366
|
+
frame = frames[-1]
|
|
367
|
+
return "%s:%s in %s" % (frame.filename, frame.lineno, frame.name)
|
|
368
|
+
except Exception:
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def trace(
|
|
373
|
+
func: Optional[F] = None,
|
|
374
|
+
*,
|
|
375
|
+
name: Optional[str] = None,
|
|
376
|
+
capture_args: bool = True,
|
|
377
|
+
capture_output: bool = True,
|
|
378
|
+
max_preview: int = 200,
|
|
379
|
+
quiet: Optional[bool] = None,
|
|
380
|
+
show_traceback: bool = False,
|
|
381
|
+
redact: Iterable[str] = (),
|
|
382
|
+
) -> Union[F, Callable[[F], F]]:
|
|
383
|
+
redact_names = frozenset(str(item).lower() for item in redact)
|
|
384
|
+
|
|
385
|
+
def decorator(target: F) -> F:
|
|
386
|
+
try:
|
|
387
|
+
signature: Optional[inspect.Signature] = inspect.signature(target)
|
|
388
|
+
except (TypeError, ValueError):
|
|
389
|
+
signature = None
|
|
390
|
+
|
|
391
|
+
config = _Config(
|
|
392
|
+
step=name or getattr(target, "__qualname__", None) or repr(target),
|
|
393
|
+
signature=signature,
|
|
394
|
+
capture_args=capture_args,
|
|
395
|
+
capture_output=capture_output,
|
|
396
|
+
max_preview=max(int(max_preview), 1),
|
|
397
|
+
quiet=quiet,
|
|
398
|
+
show_traceback=show_traceback,
|
|
399
|
+
redact=redact_names,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if inspect.iscoroutinefunction(target):
|
|
403
|
+
@functools.wraps(target)
|
|
404
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
405
|
+
span = _Span(config, args, kwargs)
|
|
406
|
+
try:
|
|
407
|
+
result = await target(*args, **kwargs)
|
|
408
|
+
except BaseException as exc:
|
|
409
|
+
span.fail(exc)
|
|
410
|
+
raise
|
|
411
|
+
span.succeed(result)
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
async_wrapper.__severn_traced__ = True # type: ignore[attr-defined]
|
|
415
|
+
return async_wrapper # type: ignore[return-value]
|
|
416
|
+
|
|
417
|
+
@functools.wraps(target)
|
|
418
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
419
|
+
span = _Span(config, args, kwargs)
|
|
420
|
+
try:
|
|
421
|
+
result = target(*args, **kwargs)
|
|
422
|
+
except BaseException as exc:
|
|
423
|
+
span.fail(exc)
|
|
424
|
+
raise
|
|
425
|
+
span.succeed(result)
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
sync_wrapper.__severn_traced__ = True # type: ignore[attr-defined]
|
|
429
|
+
return sync_wrapper # type: ignore[return-value]
|
|
430
|
+
|
|
431
|
+
if func is not None:
|
|
432
|
+
return decorator(func)
|
|
433
|
+
return decorator
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
severn/__init__.py
|
|
4
|
+
severn/cli.py
|
|
5
|
+
severn/exporter.py
|
|
6
|
+
severn/tracer.py
|
|
7
|
+
severn_trace.egg-info/PKG-INFO
|
|
8
|
+
severn_trace.egg-info/SOURCES.txt
|
|
9
|
+
severn_trace.egg-info/dependency_links.txt
|
|
10
|
+
severn_trace.egg-info/entry_points.txt
|
|
11
|
+
severn_trace.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
severn
|