onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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.
- onetool/cli.py +63 -4
- onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
- onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
- ot/__main__.py +6 -6
- ot/config/__init__.py +48 -46
- ot/config/global_templates/__init__.py +2 -2
- ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
- ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
- ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
- ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
- ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
- ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -0
- ot/config/global_templates/onetool.yaml +3 -1
- ot/config/{defaults → global_templates}/prompts.yaml +102 -97
- ot/config/global_templates/security.yaml +31 -0
- ot/config/global_templates/servers.yaml +93 -12
- ot/config/global_templates/snippets.yaml +5 -26
- ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
- ot/config/loader.py +221 -105
- ot/config/mcp.py +5 -1
- ot/config/secrets.py +192 -190
- ot/decorators.py +116 -116
- ot/executor/__init__.py +35 -35
- ot/executor/base.py +16 -16
- ot/executor/fence_processor.py +83 -83
- ot/executor/linter.py +142 -142
- ot/executor/pep723.py +288 -288
- ot/executor/runner.py +20 -6
- ot/executor/simple.py +163 -163
- ot/executor/validator.py +603 -164
- ot/http_client.py +145 -145
- ot/logging/__init__.py +37 -37
- ot/logging/entry.py +213 -213
- ot/logging/format.py +191 -188
- ot/logging/span.py +349 -349
- ot/meta.py +236 -14
- ot/paths.py +32 -49
- ot/prompts.py +218 -218
- ot/proxy/manager.py +14 -2
- ot/registry/__init__.py +189 -189
- ot/registry/parser.py +269 -269
- ot/server.py +330 -315
- ot/shortcuts/__init__.py +15 -15
- ot/shortcuts/aliases.py +87 -87
- ot/shortcuts/snippets.py +258 -258
- ot/stats/__init__.py +35 -35
- ot/stats/html.py +2 -2
- ot/stats/reader.py +354 -354
- ot/stats/timing.py +57 -57
- ot/support.py +63 -63
- ot/tools.py +1 -1
- ot/utils/batch.py +161 -161
- ot/utils/cache.py +120 -120
- ot/utils/exceptions.py +23 -23
- ot/utils/factory.py +178 -179
- ot/utils/format.py +65 -65
- ot/utils/http.py +202 -202
- ot/utils/platform.py +45 -45
- ot/utils/truncate.py +69 -69
- ot_tools/__init__.py +4 -4
- ot_tools/_convert/__init__.py +12 -12
- ot_tools/_convert/pdf.py +254 -254
- ot_tools/diagram.yaml +167 -167
- ot_tools/scaffold.py +2 -2
- ot_tools/transform.py +124 -19
- ot_tools/web_fetch.py +94 -43
- onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
- onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
- ot/config/defaults/bench.yaml +0 -4
- ot/config/defaults/onetool.yaml +0 -25
- ot/config/defaults/servers.yaml +0 -7
- ot/config/defaults/snippets.yaml +0 -4
- ot_tools/firecrawl.py +0 -732
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/logging/span.py
CHANGED
|
@@ -1,349 +1,349 @@
|
|
|
1
|
-
"""LogSpan context manager for auto-logging operations.
|
|
2
|
-
|
|
3
|
-
Wraps LogEntry and auto-logs on context exit with duration and status.
|
|
4
|
-
Supports FastMCP Context for async MCP tool execution.
|
|
5
|
-
|
|
6
|
-
Example (sync):
|
|
7
|
-
with LogSpan(span="tool.execute", tool="search") as s:
|
|
8
|
-
result = execute_tool()
|
|
9
|
-
s.add("resultCount", len(result))
|
|
10
|
-
# Auto-logs with duration, status=SUCCESS at INFO level
|
|
11
|
-
|
|
12
|
-
Example (async with FastMCP Context):
|
|
13
|
-
async with LogSpan.async_span(ctx, span="tool.execute", tool="search") as s:
|
|
14
|
-
result = await execute_tool()
|
|
15
|
-
await s.log_info("Tool completed", resultCount=len(result))
|
|
16
|
-
# Logs via FastMCP Context if available, falls back to loguru
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
from contextlib import asynccontextmanager
|
|
23
|
-
from typing import TYPE_CHECKING, Any
|
|
24
|
-
|
|
25
|
-
from loguru import logger
|
|
26
|
-
|
|
27
|
-
from ot.logging.entry import LogEntry
|
|
28
|
-
from ot.logging.format import format_log_entry
|
|
29
|
-
|
|
30
|
-
if TYPE_CHECKING:
|
|
31
|
-
from collections.abc import AsyncIterator
|
|
32
|
-
from types import TracebackType
|
|
33
|
-
|
|
34
|
-
# FastMCP Context is Any since it's an optional dependency with dynamic methods
|
|
35
|
-
Context = Any # FastMCP context with log_info, log_error, etc.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _format_for_output(entry: LogEntry) -> str:
|
|
39
|
-
"""Format a LogEntry for log output with truncation and sanitisation.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
entry: LogEntry to format
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
JSON string with formatted values
|
|
46
|
-
"""
|
|
47
|
-
# Import here to avoid circular dependency
|
|
48
|
-
from ot.config import is_log_verbose
|
|
49
|
-
|
|
50
|
-
formatted = format_log_entry(entry.to_dict(), verbose=is_log_verbose())
|
|
51
|
-
return json.dumps(formatted, separators=(",", ":"), default=str)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class LogSpan:
|
|
55
|
-
"""Context manager that wraps LogEntry and auto-logs on exit.
|
|
56
|
-
|
|
57
|
-
On successful exit, logs at INFO level with status=SUCCESS.
|
|
58
|
-
On exception, logs at ERROR level with status=FAILED, errorType, errorMessage.
|
|
59
|
-
|
|
60
|
-
Supports optional FastMCP Context for async logging in MCP tool execution.
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
def __init__(
|
|
64
|
-
self,
|
|
65
|
-
level: str = "INFO",
|
|
66
|
-
ctx: Context | None = None,
|
|
67
|
-
**initial_fields: Any,
|
|
68
|
-
) -> None:
|
|
69
|
-
"""Initialize a log span.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
level: Default log level for successful completion (default: INFO)
|
|
73
|
-
ctx: Optional FastMCP Context for async logging
|
|
74
|
-
**initial_fields: Initial fields for the underlying LogEntry
|
|
75
|
-
"""
|
|
76
|
-
self._level = level.upper()
|
|
77
|
-
self._entry = LogEntry(**initial_fields)
|
|
78
|
-
self._ctx = ctx
|
|
79
|
-
|
|
80
|
-
def add(self, key: str | None = None, value: Any = None, **kwargs: Any) -> LogSpan:
|
|
81
|
-
"""Add one or more fields to the span.
|
|
82
|
-
|
|
83
|
-
Delegates to the underlying LogEntry.
|
|
84
|
-
|
|
85
|
-
Args:
|
|
86
|
-
key: Field name (optional if using kwargs)
|
|
87
|
-
value: Field value (required if key is provided)
|
|
88
|
-
**kwargs: Bulk field additions
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Self for method chaining
|
|
92
|
-
"""
|
|
93
|
-
self._entry.add(key, value, **kwargs)
|
|
94
|
-
return self
|
|
95
|
-
|
|
96
|
-
def __setitem__(self, key: str, value: Any) -> None:
|
|
97
|
-
"""Set a field using dict-style access.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
key: Field name
|
|
101
|
-
value: Field value
|
|
102
|
-
"""
|
|
103
|
-
self._entry[key] = value
|
|
104
|
-
|
|
105
|
-
def __getitem__(self, key: str) -> Any:
|
|
106
|
-
"""Get a field using dict-style access.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
key: Field name
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
Field value
|
|
113
|
-
"""
|
|
114
|
-
return self._entry[key]
|
|
115
|
-
|
|
116
|
-
@property
|
|
117
|
-
def entry(self) -> LogEntry:
|
|
118
|
-
"""Return the underlying LogEntry for direct access.
|
|
119
|
-
|
|
120
|
-
Returns:
|
|
121
|
-
The wrapped LogEntry instance
|
|
122
|
-
"""
|
|
123
|
-
return self._entry
|
|
124
|
-
|
|
125
|
-
@property
|
|
126
|
-
def duration(self) -> float:
|
|
127
|
-
"""Return current duration since span creation.
|
|
128
|
-
|
|
129
|
-
Returns:
|
|
130
|
-
Duration in seconds
|
|
131
|
-
"""
|
|
132
|
-
return self._entry.duration
|
|
133
|
-
|
|
134
|
-
@property
|
|
135
|
-
def context(self) -> Any:
|
|
136
|
-
"""Return the FastMCP Context if available.
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
FastMCP Context or None
|
|
140
|
-
"""
|
|
141
|
-
return self._ctx
|
|
142
|
-
|
|
143
|
-
def to_dict(self) -> dict[str, Any]:
|
|
144
|
-
"""Return all fields with duration for output.
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
Dict with all fields, duration, and status info
|
|
148
|
-
"""
|
|
149
|
-
return self._entry.to_dict()
|
|
150
|
-
|
|
151
|
-
# -------------------------------------------------------------------------
|
|
152
|
-
# Sync context manager
|
|
153
|
-
# -------------------------------------------------------------------------
|
|
154
|
-
|
|
155
|
-
def __enter__(self) -> LogSpan:
|
|
156
|
-
"""Enter the span context.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Self for use in with statement
|
|
160
|
-
"""
|
|
161
|
-
return self
|
|
162
|
-
|
|
163
|
-
def __exit__(
|
|
164
|
-
self,
|
|
165
|
-
exc_type: type[BaseException] | None,
|
|
166
|
-
exc_val: BaseException | None,
|
|
167
|
-
exc_tb: TracebackType | None,
|
|
168
|
-
) -> None:
|
|
169
|
-
"""Exit the span context and auto-log.
|
|
170
|
-
|
|
171
|
-
On success (no exception), logs at the configured level with status=SUCCESS.
|
|
172
|
-
On exception, logs at ERROR level with status=FAILED and error details.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
exc_type: Exception type if an exception was raised
|
|
176
|
-
exc_val: Exception value if an exception was raised
|
|
177
|
-
exc_tb: Exception traceback if an exception was raised
|
|
178
|
-
"""
|
|
179
|
-
if exc_val is not None:
|
|
180
|
-
# Exception occurred - log as FAILED at ERROR level
|
|
181
|
-
if isinstance(exc_val, Exception):
|
|
182
|
-
self._entry.failure(error=exc_val)
|
|
183
|
-
else:
|
|
184
|
-
self._entry.failure(
|
|
185
|
-
error_type=type(exc_val).__name__, error_message=str(exc_val)
|
|
186
|
-
)
|
|
187
|
-
# depth=1 makes loguru report the caller's location, not span.py
|
|
188
|
-
logger.opt(depth=1).error(_format_for_output(self._entry))
|
|
189
|
-
else:
|
|
190
|
-
# Success - log at configured level
|
|
191
|
-
self._entry.success()
|
|
192
|
-
# depth=1 makes loguru report the caller's location, not span.py
|
|
193
|
-
logger.opt(depth=1).log(self._level, _format_for_output(self._entry))
|
|
194
|
-
|
|
195
|
-
# -------------------------------------------------------------------------
|
|
196
|
-
# Async logging methods
|
|
197
|
-
# -------------------------------------------------------------------------
|
|
198
|
-
|
|
199
|
-
async def log_debug(self, message: str, **kwargs: Any) -> None:
|
|
200
|
-
"""Log a debug message.
|
|
201
|
-
|
|
202
|
-
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
message: Log message
|
|
206
|
-
**kwargs: Additional fields to include
|
|
207
|
-
"""
|
|
208
|
-
await self._log_async("DEBUG", message, **kwargs)
|
|
209
|
-
|
|
210
|
-
async def log_info(self, message: str, **kwargs: Any) -> None:
|
|
211
|
-
"""Log an info message.
|
|
212
|
-
|
|
213
|
-
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
message: Log message
|
|
217
|
-
**kwargs: Additional fields to include
|
|
218
|
-
"""
|
|
219
|
-
await self._log_async("INFO", message, **kwargs)
|
|
220
|
-
|
|
221
|
-
async def log_warning(self, message: str, **kwargs: Any) -> None:
|
|
222
|
-
"""Log a warning message.
|
|
223
|
-
|
|
224
|
-
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
message: Log message
|
|
228
|
-
**kwargs: Additional fields to include
|
|
229
|
-
"""
|
|
230
|
-
await self._log_async("WARNING", message, **kwargs)
|
|
231
|
-
|
|
232
|
-
async def log_error(self, message: str, **kwargs: Any) -> None:
|
|
233
|
-
"""Log an error message.
|
|
234
|
-
|
|
235
|
-
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
236
|
-
|
|
237
|
-
Args:
|
|
238
|
-
message: Log message
|
|
239
|
-
**kwargs: Additional fields to include
|
|
240
|
-
"""
|
|
241
|
-
await self._log_async("ERROR", message, **kwargs)
|
|
242
|
-
|
|
243
|
-
async def _log_async(self, level: str, message: str, **kwargs: Any) -> None:
|
|
244
|
-
"""Internal async logging dispatcher.
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
level: Log level (DEBUG, INFO, WARNING, ERROR)
|
|
248
|
-
message: Log message
|
|
249
|
-
**kwargs: Additional fields
|
|
250
|
-
"""
|
|
251
|
-
# Add fields to entry
|
|
252
|
-
for key, value in kwargs.items():
|
|
253
|
-
self._entry.add(key, value)
|
|
254
|
-
|
|
255
|
-
# Try to use FastMCP Context if available
|
|
256
|
-
if self._ctx is not None:
|
|
257
|
-
try:
|
|
258
|
-
# FastMCP Context has log_* methods
|
|
259
|
-
log_method = getattr(self._ctx, f"log_{level.lower()}", None)
|
|
260
|
-
if log_method is not None and callable(log_method):
|
|
261
|
-
await log_method(message)
|
|
262
|
-
return
|
|
263
|
-
except Exception:
|
|
264
|
-
pass # Fall through to loguru
|
|
265
|
-
|
|
266
|
-
# Fallback to loguru
|
|
267
|
-
log_message = f"{message} - {self._entry}"
|
|
268
|
-
logger.opt(depth=2).log(level.upper(), log_message)
|
|
269
|
-
|
|
270
|
-
# -------------------------------------------------------------------------
|
|
271
|
-
# Async context manager
|
|
272
|
-
# -------------------------------------------------------------------------
|
|
273
|
-
|
|
274
|
-
@classmethod
|
|
275
|
-
@asynccontextmanager
|
|
276
|
-
async def async_span(
|
|
277
|
-
cls,
|
|
278
|
-
ctx: Context | None = None,
|
|
279
|
-
level: str = "INFO",
|
|
280
|
-
**initial_fields: Any,
|
|
281
|
-
) -> AsyncIterator[LogSpan]:
|
|
282
|
-
"""Create an async context manager span.
|
|
283
|
-
|
|
284
|
-
Use this for async code that needs to log via FastMCP Context.
|
|
285
|
-
|
|
286
|
-
Args:
|
|
287
|
-
ctx: Optional FastMCP Context for async logging
|
|
288
|
-
level: Default log level for successful completion
|
|
289
|
-
**initial_fields: Initial fields for the span
|
|
290
|
-
|
|
291
|
-
Yields:
|
|
292
|
-
LogSpan instance
|
|
293
|
-
|
|
294
|
-
Example:
|
|
295
|
-
async with LogSpan.async_span(ctx, span="tool.run") as span:
|
|
296
|
-
result = await run_tool()
|
|
297
|
-
span.add("result", result)
|
|
298
|
-
"""
|
|
299
|
-
span = cls(level=level, ctx=ctx, **initial_fields)
|
|
300
|
-
exc_info: tuple[type[BaseException] | None, BaseException | None, Any] = (
|
|
301
|
-
None,
|
|
302
|
-
None,
|
|
303
|
-
None,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
try:
|
|
307
|
-
yield span
|
|
308
|
-
except BaseException as e:
|
|
309
|
-
exc_info = (type(e), e, e.__traceback__)
|
|
310
|
-
raise
|
|
311
|
-
finally:
|
|
312
|
-
_exc_type, exc_val, _exc_tb = exc_info
|
|
313
|
-
|
|
314
|
-
if exc_val is not None:
|
|
315
|
-
# Exception occurred
|
|
316
|
-
if isinstance(exc_val, Exception):
|
|
317
|
-
span._entry.failure(error=exc_val)
|
|
318
|
-
else:
|
|
319
|
-
span._entry.failure(
|
|
320
|
-
error_type=type(exc_val).__name__,
|
|
321
|
-
error_message=str(exc_val),
|
|
322
|
-
)
|
|
323
|
-
|
|
324
|
-
# Log via Context or loguru
|
|
325
|
-
formatted = _format_for_output(span._entry)
|
|
326
|
-
if ctx is not None:
|
|
327
|
-
try:
|
|
328
|
-
await ctx.log_error(formatted)
|
|
329
|
-
except Exception:
|
|
330
|
-
logger.error(formatted)
|
|
331
|
-
else:
|
|
332
|
-
logger.error(formatted)
|
|
333
|
-
else:
|
|
334
|
-
# Success
|
|
335
|
-
span._entry.success()
|
|
336
|
-
|
|
337
|
-
# Log via Context or loguru
|
|
338
|
-
formatted = _format_for_output(span._entry)
|
|
339
|
-
if ctx is not None:
|
|
340
|
-
try:
|
|
341
|
-
log_method = getattr(ctx, f"log_{span._level.lower()}", None)
|
|
342
|
-
if log_method is not None and callable(log_method):
|
|
343
|
-
await log_method(formatted)
|
|
344
|
-
else:
|
|
345
|
-
await ctx.log_info(formatted)
|
|
346
|
-
except Exception:
|
|
347
|
-
logger.log(span._level, formatted)
|
|
348
|
-
else:
|
|
349
|
-
logger.log(span._level, formatted)
|
|
1
|
+
"""LogSpan context manager for auto-logging operations.
|
|
2
|
+
|
|
3
|
+
Wraps LogEntry and auto-logs on context exit with duration and status.
|
|
4
|
+
Supports FastMCP Context for async MCP tool execution.
|
|
5
|
+
|
|
6
|
+
Example (sync):
|
|
7
|
+
with LogSpan(span="tool.execute", tool="search") as s:
|
|
8
|
+
result = execute_tool()
|
|
9
|
+
s.add("resultCount", len(result))
|
|
10
|
+
# Auto-logs with duration, status=SUCCESS at INFO level
|
|
11
|
+
|
|
12
|
+
Example (async with FastMCP Context):
|
|
13
|
+
async with LogSpan.async_span(ctx, span="tool.execute", tool="search") as s:
|
|
14
|
+
result = await execute_tool()
|
|
15
|
+
await s.log_info("Tool completed", resultCount=len(result))
|
|
16
|
+
# Logs via FastMCP Context if available, falls back to loguru
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from contextlib import asynccontextmanager
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
from loguru import logger
|
|
26
|
+
|
|
27
|
+
from ot.logging.entry import LogEntry
|
|
28
|
+
from ot.logging.format import format_log_entry
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import AsyncIterator
|
|
32
|
+
from types import TracebackType
|
|
33
|
+
|
|
34
|
+
# FastMCP Context is Any since it's an optional dependency with dynamic methods
|
|
35
|
+
Context = Any # FastMCP context with log_info, log_error, etc.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _format_for_output(entry: LogEntry) -> str:
|
|
39
|
+
"""Format a LogEntry for log output with truncation and sanitisation.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
entry: LogEntry to format
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
JSON string with formatted values
|
|
46
|
+
"""
|
|
47
|
+
# Import here to avoid circular dependency
|
|
48
|
+
from ot.config import is_log_verbose
|
|
49
|
+
|
|
50
|
+
formatted = format_log_entry(entry.to_dict(), verbose=is_log_verbose())
|
|
51
|
+
return json.dumps(formatted, separators=(",", ":"), default=str)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class LogSpan:
|
|
55
|
+
"""Context manager that wraps LogEntry and auto-logs on exit.
|
|
56
|
+
|
|
57
|
+
On successful exit, logs at INFO level with status=SUCCESS.
|
|
58
|
+
On exception, logs at ERROR level with status=FAILED, errorType, errorMessage.
|
|
59
|
+
|
|
60
|
+
Supports optional FastMCP Context for async logging in MCP tool execution.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
level: str = "INFO",
|
|
66
|
+
ctx: Context | None = None,
|
|
67
|
+
**initial_fields: Any,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Initialize a log span.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
level: Default log level for successful completion (default: INFO)
|
|
73
|
+
ctx: Optional FastMCP Context for async logging
|
|
74
|
+
**initial_fields: Initial fields for the underlying LogEntry
|
|
75
|
+
"""
|
|
76
|
+
self._level = level.upper()
|
|
77
|
+
self._entry = LogEntry(**initial_fields)
|
|
78
|
+
self._ctx = ctx
|
|
79
|
+
|
|
80
|
+
def add(self, key: str | None = None, value: Any = None, **kwargs: Any) -> LogSpan:
|
|
81
|
+
"""Add one or more fields to the span.
|
|
82
|
+
|
|
83
|
+
Delegates to the underlying LogEntry.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
key: Field name (optional if using kwargs)
|
|
87
|
+
value: Field value (required if key is provided)
|
|
88
|
+
**kwargs: Bulk field additions
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Self for method chaining
|
|
92
|
+
"""
|
|
93
|
+
self._entry.add(key, value, **kwargs)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
97
|
+
"""Set a field using dict-style access.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
key: Field name
|
|
101
|
+
value: Field value
|
|
102
|
+
"""
|
|
103
|
+
self._entry[key] = value
|
|
104
|
+
|
|
105
|
+
def __getitem__(self, key: str) -> Any:
|
|
106
|
+
"""Get a field using dict-style access.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
key: Field name
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Field value
|
|
113
|
+
"""
|
|
114
|
+
return self._entry[key]
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def entry(self) -> LogEntry:
|
|
118
|
+
"""Return the underlying LogEntry for direct access.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The wrapped LogEntry instance
|
|
122
|
+
"""
|
|
123
|
+
return self._entry
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def duration(self) -> float:
|
|
127
|
+
"""Return current duration since span creation.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Duration in seconds
|
|
131
|
+
"""
|
|
132
|
+
return self._entry.duration
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def context(self) -> Any:
|
|
136
|
+
"""Return the FastMCP Context if available.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
FastMCP Context or None
|
|
140
|
+
"""
|
|
141
|
+
return self._ctx
|
|
142
|
+
|
|
143
|
+
def to_dict(self) -> dict[str, Any]:
|
|
144
|
+
"""Return all fields with duration for output.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Dict with all fields, duration, and status info
|
|
148
|
+
"""
|
|
149
|
+
return self._entry.to_dict()
|
|
150
|
+
|
|
151
|
+
# -------------------------------------------------------------------------
|
|
152
|
+
# Sync context manager
|
|
153
|
+
# -------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def __enter__(self) -> LogSpan:
|
|
156
|
+
"""Enter the span context.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Self for use in with statement
|
|
160
|
+
"""
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def __exit__(
|
|
164
|
+
self,
|
|
165
|
+
exc_type: type[BaseException] | None,
|
|
166
|
+
exc_val: BaseException | None,
|
|
167
|
+
exc_tb: TracebackType | None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Exit the span context and auto-log.
|
|
170
|
+
|
|
171
|
+
On success (no exception), logs at the configured level with status=SUCCESS.
|
|
172
|
+
On exception, logs at ERROR level with status=FAILED and error details.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
exc_type: Exception type if an exception was raised
|
|
176
|
+
exc_val: Exception value if an exception was raised
|
|
177
|
+
exc_tb: Exception traceback if an exception was raised
|
|
178
|
+
"""
|
|
179
|
+
if exc_val is not None:
|
|
180
|
+
# Exception occurred - log as FAILED at ERROR level
|
|
181
|
+
if isinstance(exc_val, Exception):
|
|
182
|
+
self._entry.failure(error=exc_val)
|
|
183
|
+
else:
|
|
184
|
+
self._entry.failure(
|
|
185
|
+
error_type=type(exc_val).__name__, error_message=str(exc_val)
|
|
186
|
+
)
|
|
187
|
+
# depth=1 makes loguru report the caller's location, not span.py
|
|
188
|
+
logger.opt(depth=1).error(_format_for_output(self._entry))
|
|
189
|
+
else:
|
|
190
|
+
# Success - log at configured level
|
|
191
|
+
self._entry.success()
|
|
192
|
+
# depth=1 makes loguru report the caller's location, not span.py
|
|
193
|
+
logger.opt(depth=1).log(self._level, _format_for_output(self._entry))
|
|
194
|
+
|
|
195
|
+
# -------------------------------------------------------------------------
|
|
196
|
+
# Async logging methods
|
|
197
|
+
# -------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
async def log_debug(self, message: str, **kwargs: Any) -> None:
|
|
200
|
+
"""Log a debug message.
|
|
201
|
+
|
|
202
|
+
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
message: Log message
|
|
206
|
+
**kwargs: Additional fields to include
|
|
207
|
+
"""
|
|
208
|
+
await self._log_async("DEBUG", message, **kwargs)
|
|
209
|
+
|
|
210
|
+
async def log_info(self, message: str, **kwargs: Any) -> None:
|
|
211
|
+
"""Log an info message.
|
|
212
|
+
|
|
213
|
+
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
message: Log message
|
|
217
|
+
**kwargs: Additional fields to include
|
|
218
|
+
"""
|
|
219
|
+
await self._log_async("INFO", message, **kwargs)
|
|
220
|
+
|
|
221
|
+
async def log_warning(self, message: str, **kwargs: Any) -> None:
|
|
222
|
+
"""Log a warning message.
|
|
223
|
+
|
|
224
|
+
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
message: Log message
|
|
228
|
+
**kwargs: Additional fields to include
|
|
229
|
+
"""
|
|
230
|
+
await self._log_async("WARNING", message, **kwargs)
|
|
231
|
+
|
|
232
|
+
async def log_error(self, message: str, **kwargs: Any) -> None:
|
|
233
|
+
"""Log an error message.
|
|
234
|
+
|
|
235
|
+
Dispatches to FastMCP Context if available, otherwise uses loguru.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
message: Log message
|
|
239
|
+
**kwargs: Additional fields to include
|
|
240
|
+
"""
|
|
241
|
+
await self._log_async("ERROR", message, **kwargs)
|
|
242
|
+
|
|
243
|
+
async def _log_async(self, level: str, message: str, **kwargs: Any) -> None:
|
|
244
|
+
"""Internal async logging dispatcher.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR)
|
|
248
|
+
message: Log message
|
|
249
|
+
**kwargs: Additional fields
|
|
250
|
+
"""
|
|
251
|
+
# Add fields to entry
|
|
252
|
+
for key, value in kwargs.items():
|
|
253
|
+
self._entry.add(key, value)
|
|
254
|
+
|
|
255
|
+
# Try to use FastMCP Context if available
|
|
256
|
+
if self._ctx is not None:
|
|
257
|
+
try:
|
|
258
|
+
# FastMCP Context has log_* methods
|
|
259
|
+
log_method = getattr(self._ctx, f"log_{level.lower()}", None)
|
|
260
|
+
if log_method is not None and callable(log_method):
|
|
261
|
+
await log_method(message)
|
|
262
|
+
return
|
|
263
|
+
except Exception:
|
|
264
|
+
pass # Fall through to loguru
|
|
265
|
+
|
|
266
|
+
# Fallback to loguru
|
|
267
|
+
log_message = f"{message} - {self._entry}"
|
|
268
|
+
logger.opt(depth=2).log(level.upper(), log_message)
|
|
269
|
+
|
|
270
|
+
# -------------------------------------------------------------------------
|
|
271
|
+
# Async context manager
|
|
272
|
+
# -------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
@classmethod
|
|
275
|
+
@asynccontextmanager
|
|
276
|
+
async def async_span(
|
|
277
|
+
cls,
|
|
278
|
+
ctx: Context | None = None,
|
|
279
|
+
level: str = "INFO",
|
|
280
|
+
**initial_fields: Any,
|
|
281
|
+
) -> AsyncIterator[LogSpan]:
|
|
282
|
+
"""Create an async context manager span.
|
|
283
|
+
|
|
284
|
+
Use this for async code that needs to log via FastMCP Context.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
ctx: Optional FastMCP Context for async logging
|
|
288
|
+
level: Default log level for successful completion
|
|
289
|
+
**initial_fields: Initial fields for the span
|
|
290
|
+
|
|
291
|
+
Yields:
|
|
292
|
+
LogSpan instance
|
|
293
|
+
|
|
294
|
+
Example:
|
|
295
|
+
async with LogSpan.async_span(ctx, span="tool.run") as span:
|
|
296
|
+
result = await run_tool()
|
|
297
|
+
span.add("result", result)
|
|
298
|
+
"""
|
|
299
|
+
span = cls(level=level, ctx=ctx, **initial_fields)
|
|
300
|
+
exc_info: tuple[type[BaseException] | None, BaseException | None, Any] = (
|
|
301
|
+
None,
|
|
302
|
+
None,
|
|
303
|
+
None,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
yield span
|
|
308
|
+
except BaseException as e:
|
|
309
|
+
exc_info = (type(e), e, e.__traceback__)
|
|
310
|
+
raise
|
|
311
|
+
finally:
|
|
312
|
+
_exc_type, exc_val, _exc_tb = exc_info
|
|
313
|
+
|
|
314
|
+
if exc_val is not None:
|
|
315
|
+
# Exception occurred
|
|
316
|
+
if isinstance(exc_val, Exception):
|
|
317
|
+
span._entry.failure(error=exc_val)
|
|
318
|
+
else:
|
|
319
|
+
span._entry.failure(
|
|
320
|
+
error_type=type(exc_val).__name__,
|
|
321
|
+
error_message=str(exc_val),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Log via Context or loguru
|
|
325
|
+
formatted = _format_for_output(span._entry)
|
|
326
|
+
if ctx is not None:
|
|
327
|
+
try:
|
|
328
|
+
await ctx.log_error(formatted)
|
|
329
|
+
except Exception:
|
|
330
|
+
logger.error(formatted)
|
|
331
|
+
else:
|
|
332
|
+
logger.error(formatted)
|
|
333
|
+
else:
|
|
334
|
+
# Success
|
|
335
|
+
span._entry.success()
|
|
336
|
+
|
|
337
|
+
# Log via Context or loguru
|
|
338
|
+
formatted = _format_for_output(span._entry)
|
|
339
|
+
if ctx is not None:
|
|
340
|
+
try:
|
|
341
|
+
log_method = getattr(ctx, f"log_{span._level.lower()}", None)
|
|
342
|
+
if log_method is not None and callable(log_method):
|
|
343
|
+
await log_method(formatted)
|
|
344
|
+
else:
|
|
345
|
+
await ctx.log_info(formatted)
|
|
346
|
+
except Exception:
|
|
347
|
+
logger.log(span._level, formatted)
|
|
348
|
+
else:
|
|
349
|
+
logger.log(span._level, formatted)
|