Logging Guide¶
This guide covers OneTool's structured logging infrastructure built on Loguru.
Quick Start¶
from ot.logging import configure_logging, LogSpan
# Initialize logging for your CLI
configure_logging(log_name="my-cli")
# Use LogSpan for structured operation logging
with LogSpan(span="operation.name", key="value") as s:
result = do_something()
s.add("resultCount", len(result))
# Logs automatically on exit with duration and status
Core Components¶
configure_logging(log_name)¶
Initializes Loguru for file-only output with dev-friendly formatting.
from ot.logging import configure_logging
# In your CLI entry point
configure_logging(log_name="serve") # Creates logs/serve.log
Environment variables:
- OT_LOG_LEVEL: Log level (default: INFO)
- OT_LOG_DIR: Directory for log files (default: ../logs, relative to config dir)
- OT_LOG_VERBOSE: Disable truncation, show full values (default: false)
LogSpan¶
Context manager that wraps LogEntry and auto-logs on exit with duration and status.
from ot.logging import LogSpan
# Sync usage
with LogSpan(span="tool.execute", tool="search") as s:
result = execute_tool()
s.add("resultCount", len(result))
# Logs at INFO level with status=SUCCESS and duration
# With exception handling
with LogSpan(span="api.request", url=url):
response = make_request()
# On exception: logs at ERROR level with status=FAILED, errorType, errorMessage
Async usage with FastMCP Context:
async with LogSpan.async_span(ctx, span="tool.execute", tool="search") as s:
result = await execute_tool()
await s.log_info("Tool completed", resultCount=len(result))
LogEntry¶
Low-level structured log entry with fluent API.
from ot.logging import LogEntry
entry = LogEntry(span="operation", key="value")
entry.add("extra", data)
entry.success() # or entry.failure(error=exc)
logger.info(str(entry))
Span Naming Conventions¶
Span names use dot-notation: {component}.{operation}[.{detail}]
Server Operations (serve-observability)¶
mcp.server.start- Server startupmcp.server.stop- Server shutdowntool.lookup- Tool resolution
CLI Operations¶
browse.session.start- Browser session startbrowse.navigate- Navigationbrowse.screenshot- Screenshot capture
Tool Operations¶
See Creating Tools for tool span naming conventions.
Examples¶
CLI Initialisation¶
# In src/ot_browse/app.py
from ot.logging import configure_logging
def main() -> None:
configure_logging(log_name="browse")
# ... rest of CLI
Tool Functions¶
See Creating Tools for comprehensive tool logging examples.
Async MCP Tool¶
from ot.logging import LogSpan
async def execute_tool(ctx, tool_name: str, args: dict) -> str:
async with LogSpan.async_span(ctx, span="tool.execute", tool=tool_name) as s:
tool = registry.get(tool_name)
if not tool:
s.add("error", "not_found")
return f"Tool {tool_name} not found"
result = await tool.call(**args)
s.add("resultLen", len(result))
return result
Nested Spans¶
with LogSpan(span="browse.session", source=source) as outer:
# Navigate
with LogSpan(span="browse.navigate", url=url) as nav:
page = navigate_to(url)
nav.add("status", page.status)
# Capture
with LogSpan(span="browse.capture", page=page.url) as cap:
files = capture_page(page)
cap.add("fileCount", len(files))
outer.add("success", True)
Log Output¶
Logs are written in dev-friendly format to logs/{log_name}.log (relative to config directory):
12:34:56.789 | INFO | server:54 | mcp.server.start | status=SUCCESS | duration=0.042
12:34:57.123 | INFO | brave:78 | brave.search.web | query=test | resultCount=10 | duration=1.234
12:34:58.456 | ERROR | web:92 | web.fetch | url=http://... | status=FAILED | errorType=HTTPError
Configuration¶
Log Levels¶
Set via log_level in ot-serve.yaml or OT_LOG_LEVEL environment variable:
| Level | Use Case |
|---|---|
DEBUG |
Verbose debugging (development only) |
INFO |
Normal operation (default) |
WARNING |
Potential issues |
ERROR |
Failures requiring attention |
Log Directory¶
Set via log_dir in ot-serve.yaml or OT_LOG_DIR environment variable:
- Default:
../logs(relative to config directory) - Automatically created if it doesn't exist
- Supports
~expansion for home directory
File Rotation¶
Production logs use automatic rotation:
Test Logging¶
For tests, use configure_test_logging() instead:
from ot.logging import configure_test_logging
# In conftest.py or test setup
configure_test_logging(
module_name="test_tools",
dev_output=True, # Dev-friendly format to stderr
dev_file=False, # No separate dev log file
)
This creates:
- logs/{module_name}.log - JSON structured logs
- Optional logs/{module_name}.dev.log - Dev-friendly format (if dev_file=True)
Logger Interception¶
The logging system intercepts standard Python logging and redirects to Loguru:
Intercepted loggers (redirected to Loguru):
- fastmcp, mcp, uvicorn
Silenced loggers (set to WARNING level):
- httpcore, httpx, hpack - HTTP transport noise
- openai, openai._base_client - API client noise
- anyio, mcp - Async framework noise
Output Formatting¶
Log output is automatically formatted with truncation and credential sanitization at output time. Full values are preserved in LogEntry for programmatic access.
Truncation Limits¶
Field-based truncation limits (applied unless OT_LOG_VERBOSE=true):
| Field Pattern | Limit |
|---|---|
| path, filepath, source, dest, directory | 200 |
| url | 120 |
| query, topic, pattern | 100 |
| error | 300 |
| default | 120 |
Credential Sanitization¶
URLs with embedded credentials are automatically masked:
Applied to:
- Fields containing "url" in the name
- String values starting with http:// or https://
Verbose Mode¶
Disable truncation with OT_LOG_VERBOSE=true or log_verbose: true in config:
Credentials are always sanitized, even in verbose mode.
Formatting Functions¶
from ot.logging import format_log_entry, sanitize_url, format_value
# Format entire log entry
formatted = format_log_entry(entry.to_dict(), verbose=False)
# Sanitize a single URL
safe_url = sanitize_url("postgres://user:pass@host/db")
# Truncate a value with field-based limit
truncated = format_value(long_string, field_name="query")