onetool-mcp 1.0.0b1__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.
Files changed (132) hide show
  1. bench/__init__.py +5 -0
  2. bench/cli.py +69 -0
  3. bench/harness/__init__.py +66 -0
  4. bench/harness/client.py +692 -0
  5. bench/harness/config.py +397 -0
  6. bench/harness/csv_writer.py +109 -0
  7. bench/harness/evaluate.py +512 -0
  8. bench/harness/metrics.py +283 -0
  9. bench/harness/runner.py +899 -0
  10. bench/py.typed +0 -0
  11. bench/reporter.py +629 -0
  12. bench/run.py +487 -0
  13. bench/secrets.py +101 -0
  14. bench/utils.py +16 -0
  15. onetool/__init__.py +4 -0
  16. onetool/cli.py +391 -0
  17. onetool/py.typed +0 -0
  18. onetool_mcp-1.0.0b1.dist-info/METADATA +163 -0
  19. onetool_mcp-1.0.0b1.dist-info/RECORD +132 -0
  20. onetool_mcp-1.0.0b1.dist-info/WHEEL +4 -0
  21. onetool_mcp-1.0.0b1.dist-info/entry_points.txt +3 -0
  22. onetool_mcp-1.0.0b1.dist-info/licenses/LICENSE.txt +687 -0
  23. onetool_mcp-1.0.0b1.dist-info/licenses/NOTICE.txt +64 -0
  24. ot/__init__.py +37 -0
  25. ot/__main__.py +6 -0
  26. ot/_cli.py +107 -0
  27. ot/_tui.py +53 -0
  28. ot/config/__init__.py +46 -0
  29. ot/config/defaults/bench.yaml +4 -0
  30. ot/config/defaults/diagram-templates/api-flow.mmd +33 -0
  31. ot/config/defaults/diagram-templates/c4-context.puml +30 -0
  32. ot/config/defaults/diagram-templates/class-diagram.mmd +87 -0
  33. ot/config/defaults/diagram-templates/feature-mindmap.mmd +70 -0
  34. ot/config/defaults/diagram-templates/microservices.d2 +81 -0
  35. ot/config/defaults/diagram-templates/project-gantt.mmd +37 -0
  36. ot/config/defaults/diagram-templates/state-machine.mmd +42 -0
  37. ot/config/defaults/onetool.yaml +25 -0
  38. ot/config/defaults/prompts.yaml +97 -0
  39. ot/config/defaults/servers.yaml +7 -0
  40. ot/config/defaults/snippets.yaml +4 -0
  41. ot/config/defaults/tool_templates/__init__.py +7 -0
  42. ot/config/defaults/tool_templates/extension.py +52 -0
  43. ot/config/defaults/tool_templates/isolated.py +61 -0
  44. ot/config/dynamic.py +121 -0
  45. ot/config/global_templates/__init__.py +2 -0
  46. ot/config/global_templates/bench-secrets-template.yaml +6 -0
  47. ot/config/global_templates/bench.yaml +9 -0
  48. ot/config/global_templates/onetool.yaml +27 -0
  49. ot/config/global_templates/secrets-template.yaml +44 -0
  50. ot/config/global_templates/servers.yaml +18 -0
  51. ot/config/global_templates/snippets.yaml +235 -0
  52. ot/config/loader.py +1087 -0
  53. ot/config/mcp.py +145 -0
  54. ot/config/secrets.py +190 -0
  55. ot/config/tool_config.py +125 -0
  56. ot/decorators.py +116 -0
  57. ot/executor/__init__.py +35 -0
  58. ot/executor/base.py +16 -0
  59. ot/executor/fence_processor.py +83 -0
  60. ot/executor/linter.py +142 -0
  61. ot/executor/pack_proxy.py +260 -0
  62. ot/executor/param_resolver.py +140 -0
  63. ot/executor/pep723.py +288 -0
  64. ot/executor/result_store.py +369 -0
  65. ot/executor/runner.py +496 -0
  66. ot/executor/simple.py +163 -0
  67. ot/executor/tool_loader.py +396 -0
  68. ot/executor/validator.py +398 -0
  69. ot/executor/worker_pool.py +388 -0
  70. ot/executor/worker_proxy.py +189 -0
  71. ot/http_client.py +145 -0
  72. ot/logging/__init__.py +37 -0
  73. ot/logging/config.py +315 -0
  74. ot/logging/entry.py +213 -0
  75. ot/logging/format.py +188 -0
  76. ot/logging/span.py +349 -0
  77. ot/meta.py +1555 -0
  78. ot/paths.py +453 -0
  79. ot/prompts.py +218 -0
  80. ot/proxy/__init__.py +21 -0
  81. ot/proxy/manager.py +396 -0
  82. ot/py.typed +0 -0
  83. ot/registry/__init__.py +189 -0
  84. ot/registry/models.py +57 -0
  85. ot/registry/parser.py +269 -0
  86. ot/registry/registry.py +413 -0
  87. ot/server.py +315 -0
  88. ot/shortcuts/__init__.py +15 -0
  89. ot/shortcuts/aliases.py +87 -0
  90. ot/shortcuts/snippets.py +258 -0
  91. ot/stats/__init__.py +35 -0
  92. ot/stats/html.py +250 -0
  93. ot/stats/jsonl_writer.py +283 -0
  94. ot/stats/reader.py +354 -0
  95. ot/stats/timing.py +57 -0
  96. ot/support.py +63 -0
  97. ot/tools.py +114 -0
  98. ot/utils/__init__.py +81 -0
  99. ot/utils/batch.py +161 -0
  100. ot/utils/cache.py +120 -0
  101. ot/utils/deps.py +403 -0
  102. ot/utils/exceptions.py +23 -0
  103. ot/utils/factory.py +179 -0
  104. ot/utils/format.py +65 -0
  105. ot/utils/http.py +202 -0
  106. ot/utils/platform.py +45 -0
  107. ot/utils/sanitize.py +130 -0
  108. ot/utils/truncate.py +69 -0
  109. ot_tools/__init__.py +4 -0
  110. ot_tools/_convert/__init__.py +12 -0
  111. ot_tools/_convert/excel.py +279 -0
  112. ot_tools/_convert/pdf.py +254 -0
  113. ot_tools/_convert/powerpoint.py +268 -0
  114. ot_tools/_convert/utils.py +358 -0
  115. ot_tools/_convert/word.py +283 -0
  116. ot_tools/brave_search.py +604 -0
  117. ot_tools/code_search.py +736 -0
  118. ot_tools/context7.py +495 -0
  119. ot_tools/convert.py +614 -0
  120. ot_tools/db.py +415 -0
  121. ot_tools/diagram.py +1604 -0
  122. ot_tools/diagram.yaml +167 -0
  123. ot_tools/excel.py +1372 -0
  124. ot_tools/file.py +1348 -0
  125. ot_tools/firecrawl.py +732 -0
  126. ot_tools/grounding_search.py +646 -0
  127. ot_tools/package.py +604 -0
  128. ot_tools/py.typed +0 -0
  129. ot_tools/ripgrep.py +544 -0
  130. ot_tools/scaffold.py +471 -0
  131. ot_tools/transform.py +213 -0
  132. ot_tools/web_fetch.py +384 -0
ot/logging/format.py ADDED
@@ -0,0 +1,188 @@
1
+ """Output formatting for log entries.
2
+
3
+ Provides truncation and credential sanitisation at OUTPUT time.
4
+ Full values are preserved in LogEntry for programmatic access.
5
+
6
+ Field-based truncation limits:
7
+ | Pattern | Limit |
8
+ |------------------------------------------------|-------|
9
+ | path, filepath, source, dest, directory | 200 |
10
+ | url | 120 |
11
+ | query, topic | 100 |
12
+ | pattern | 100 |
13
+ | error | 300 |
14
+ | default | 120 |
15
+
16
+ Credential sanitisation:
17
+ - URLs with credentials: postgres://user:pass@host -> postgres://***:***@host
18
+ - Applied to fields containing 'url' or values starting with http(s)://
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from typing import Any
25
+ from urllib.parse import urlparse, urlunparse
26
+
27
+ # Field name patterns mapped to truncation limits
28
+ FIELD_LIMITS: dict[str, int] = {
29
+ "path": 200,
30
+ "filepath": 200,
31
+ "source": 200,
32
+ "dest": 200,
33
+ "directory": 200,
34
+ "url": 120,
35
+ "query": 100,
36
+ "topic": 100,
37
+ "pattern": 100,
38
+ "error": 300,
39
+ }
40
+ DEFAULT_LIMIT = 120
41
+
42
+ # URL credential pattern: scheme://user:pass@host
43
+ URL_WITH_CREDS = re.compile(r"^([a-zA-Z][a-zA-Z0-9+.-]*://)([^:]+):([^@]+)@(.+)$")
44
+
45
+
46
+ def _get_field_limit(field_name: str) -> int:
47
+ """Get truncation limit for a field based on name patterns.
48
+
49
+ Args:
50
+ field_name: Name of the field (case-insensitive matching)
51
+
52
+ Returns:
53
+ Truncation limit in characters
54
+ """
55
+ lower_name = field_name.lower()
56
+
57
+ # Check for pattern matches in field name
58
+ for pattern, limit in FIELD_LIMITS.items():
59
+ if pattern in lower_name:
60
+ return limit
61
+
62
+ return DEFAULT_LIMIT
63
+
64
+
65
+ def sanitize_url(url: str) -> str:
66
+ """Mask credentials in a URL.
67
+
68
+ Args:
69
+ url: URL string, potentially with embedded credentials
70
+
71
+ Returns:
72
+ URL with username:password masked as ***:***
73
+
74
+ Example:
75
+ postgres://user:password@host/db -> postgres://***:***@host/db
76
+ """
77
+ match = URL_WITH_CREDS.match(url)
78
+ if match:
79
+ scheme, _user, _password, rest = match.groups()
80
+ return f"{scheme}***:***@{rest}"
81
+
82
+ # Also handle parsed URLs without regex (for edge cases)
83
+ try:
84
+ parsed = urlparse(url)
85
+ if parsed.username or parsed.password:
86
+ # Reconstruct with masked credentials
87
+ netloc = "***:***@" if parsed.password else "***@"
88
+ netloc += parsed.hostname or ""
89
+ if parsed.port:
90
+ netloc += f":{parsed.port}"
91
+ return urlunparse(
92
+ (
93
+ parsed.scheme,
94
+ netloc,
95
+ parsed.path,
96
+ parsed.params,
97
+ parsed.query,
98
+ parsed.fragment,
99
+ )
100
+ )
101
+ except Exception:
102
+ pass # Return original if parsing fails
103
+
104
+ return url
105
+
106
+
107
+ def format_value(value: Any, field_name: str = "", max_length: int | None = None) -> Any:
108
+ """Format a single value for output with truncation.
109
+
110
+ Only string values are truncated. Other types pass through unchanged.
111
+
112
+ Args:
113
+ value: Value to format
114
+ field_name: Field name for determining truncation limit
115
+ max_length: Override truncation limit (None = use field-based limit)
116
+
117
+ Returns:
118
+ Formatted value (truncated string or original value)
119
+ """
120
+ if not isinstance(value, str):
121
+ return value
122
+
123
+ if max_length is None:
124
+ max_length = _get_field_limit(field_name)
125
+
126
+ if len(value) <= max_length:
127
+ return value
128
+
129
+ # Truncate with ellipsis
130
+ return value[: max_length - 3] + "..."
131
+
132
+
133
+ def sanitize_for_output(value: Any, field_name: str = "") -> Any:
134
+ """Sanitize a value by masking credentials.
135
+
136
+ Applies to:
137
+ - Fields containing 'url' in name
138
+ - String values starting with http:// or https://
139
+
140
+ Args:
141
+ value: Value to sanitize
142
+ field_name: Field name for context
143
+
144
+ Returns:
145
+ Sanitized value
146
+ """
147
+ if not isinstance(value, str):
148
+ return value
149
+
150
+ lower_name = field_name.lower()
151
+ lower_value = value.lower()
152
+
153
+ # Apply URL sanitisation if field name contains 'url' or value is a URL
154
+ if "url" in lower_name or lower_value.startswith(("http://", "https://")):
155
+ return sanitize_url(value)
156
+
157
+ return value
158
+
159
+
160
+ def format_log_entry(
161
+ entry_dict: dict[str, Any],
162
+ verbose: bool = False,
163
+ ) -> dict[str, Any]:
164
+ """Format a log entry dict for output.
165
+
166
+ Applies truncation and credential sanitisation to all fields.
167
+ Full values are preserved in the original entry.
168
+
169
+ Args:
170
+ entry_dict: Log entry as dict (from LogEntry.to_dict())
171
+ verbose: If True, skip truncation (still sanitizes credentials)
172
+
173
+ Returns:
174
+ New dict with formatted values
175
+ """
176
+ formatted: dict[str, Any] = {}
177
+
178
+ for key, value in entry_dict.items():
179
+ # Always sanitize credentials
180
+ sanitized = sanitize_for_output(value, key)
181
+
182
+ # Apply truncation unless verbose mode
183
+ if verbose:
184
+ formatted[key] = sanitized
185
+ else:
186
+ formatted[key] = format_value(sanitized, key)
187
+
188
+ return formatted
ot/logging/span.py ADDED
@@ -0,0 +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)