Skip to content

Commit c1c2a48

Browse files
authored
feat(mcp): add observability decorators for automatic tool instrumentation (#172)
* feat(mcp): add @observe_tool and @observe_service decorators with tests * feat(mcp): apply @observe_tool decorator to all foundation tools * docs(mcp): add comprehensive documentation for observability decorators to LOGGING.md
1 parent 36a2b0f commit c1c2a48

9 files changed

Lines changed: 741 additions & 13 deletions

File tree

katana_mcp_server/docs/LOGGING.md

Lines changed: 255 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
# Structured Logging in Katana MCP Server
22

3-
The Katana MCP Server uses structured logging with [structlog](https://www.structlog.org/) to provide rich observability and debugging capabilities.
3+
The Katana MCP Server uses structured logging with
4+
[structlog](https://www.structlog.org/) to provide rich observability and debugging
5+
capabilities.
46

57
## Features
68

7-
- **Structured JSON or text output** - Choose between JSON (for log aggregation) or human-readable text format
8-
- **Contextual information** - Every log includes relevant context (tool names, SKUs, IDs, etc.)
9+
- **Structured JSON or text output** - Choose between JSON (for log aggregation) or
10+
human-readable text format
11+
- **Contextual information** - Every log includes relevant context (tool names, SKUs,
12+
IDs, etc.)
913
- **Performance metrics** - Automatic duration tracking for all tool executions
1014
- **Trace IDs** - Support for request correlation across operations
11-
- **Security** - Automatic redaction of sensitive data (API keys, passwords, credentials)
15+
- **Security** - Automatic redaction of sensitive data (API keys, passwords,
16+
credentials)
1217
- **Configurable levels** - Control log verbosity via environment variables
1318

1419
## Configuration
@@ -18,21 +23,25 @@ The Katana MCP Server uses structured logging with [structlog](https://www.struc
1823
Configure logging behavior with these environment variables:
1924

2025
- **`KATANA_MCP_LOG_LEVEL`** - Log level (default: `INFO`)
26+
2127
- Options: `DEBUG`, `INFO`, `WARNING`, `ERROR`
22-
28+
2329
- **`KATANA_MCP_LOG_FORMAT`** - Output format (default: `json`)
30+
2431
- Options: `json`, `text`
2532

2633
### Examples
2734

2835
**Development (verbose text output):**
36+
2937
```bash
3038
export KATANA_MCP_LOG_LEVEL=DEBUG
3139
export KATANA_MCP_LOG_FORMAT=text
3240
katana-mcp-server
3341
```
3442

3543
**Production (structured JSON):**
44+
3645
```bash
3746
export KATANA_MCP_LOG_LEVEL=INFO
3847
export KATANA_MCP_LOG_FORMAT=json
@@ -66,6 +75,7 @@ katana-mcp-server
6675
### Server Lifecycle
6776

6877
**Server Initialization:**
78+
6979
```json
7080
{
7181
"event": "server_initializing",
@@ -76,6 +86,7 @@ katana-mcp-server
7686
```
7787

7888
**Client Ready:**
89+
7990
```json
8091
{
8192
"event": "client_initialized",
@@ -87,6 +98,7 @@ katana-mcp-server
8798
```
8899

89100
**Server Ready:**
101+
90102
```json
91103
{
92104
"event": "server_ready",
@@ -98,6 +110,7 @@ katana-mcp-server
98110
### Tool Execution
99111

100112
**Inventory Check (Success):**
113+
101114
```json
102115
{
103116
"event": "inventory_check_completed",
@@ -111,6 +124,7 @@ katana-mcp-server
111124
```
112125

113126
**Search Items (Success):**
127+
114128
```json
115129
{
116130
"event": "item_search_completed",
@@ -122,6 +136,7 @@ katana-mcp-server
122136
```
123137

124138
**Create Item (Success):**
139+
125140
```json
126141
{
127142
"event": "item_create_completed",
@@ -137,6 +152,7 @@ katana-mcp-server
137152
### Error Logging
138153

139154
**Tool Failure:**
155+
140156
```json
141157
{
142158
"event": "item_search_failed",
@@ -150,6 +166,7 @@ katana-mcp-server
150166
```
151167

152168
**Authentication Error:**
169+
153170
```json
154171
{
155172
"event": "authentication_failed",
@@ -166,11 +183,13 @@ katana-mcp-server
166183
The logger automatically redacts sensitive information from logs:
167184

168185
**Input:**
186+
169187
```python
170188
logger.info("api_call", api_key="secret-key-123", username="john")
171189
```
172190

173191
**Output (JSON):**
192+
174193
```json
175194
{
176195
"event": "api_call",
@@ -181,6 +200,7 @@ logger.info("api_call", api_key="secret-key-123", username="john")
181200
```
182201

183202
**Redacted Keys:**
203+
184204
- `api_key`, `API_KEY`
185205
- `password`, `PASSWORD`
186206
- `secret`, `SECRET`
@@ -199,6 +219,7 @@ All tool executions include performance metrics:
199219
- **`threshold`** - Configured threshold (for low stock checks)
200220

201221
Example with metrics:
222+
202223
```json
203224
{
204225
"event": "low_stock_search_completed",
@@ -210,14 +231,238 @@ Example with metrics:
210231
}
211232
```
212233

234+
## Observability Decorators
235+
236+
The Katana MCP Server provides convenience decorators to automatically instrument tools
237+
and service methods with logging, timing, and error tracking.
238+
239+
### @observe_tool
240+
241+
Automatically instruments MCP tool functions with comprehensive observability.
242+
243+
**Features:**
244+
245+
- Automatic logging of tool invocations with parameters
246+
- Execution timing in milliseconds
247+
- Success/failure tracking
248+
- Error details with exception types
249+
- Context parameter filtering (ctx/context excluded from logs)
250+
251+
**Usage:**
252+
253+
```python
254+
from katana_mcp.logging import observe_tool
255+
from fastmcp import Context
256+
257+
@observe_tool
258+
@mcp.tool()
259+
async def my_tool(request: MyRequest, context: Context) -> MyResponse:
260+
"""My tool implementation."""
261+
return await do_work()
262+
```
263+
264+
**Log Events Produced:**
265+
266+
Tool invocation (INFO level):
267+
268+
```json
269+
{
270+
"event": "tool_invoked",
271+
"tool_name": "my_tool",
272+
"params": {"request": {...}},
273+
"timestamp": "2025-01-05T17:08:40.123456Z",
274+
"level": "info"
275+
}
276+
```
277+
278+
Successful completion (INFO level):
279+
280+
```json
281+
{
282+
"event": "tool_completed",
283+
"tool_name": "my_tool",
284+
"duration_ms": 245.67,
285+
"success": true,
286+
"timestamp": "2025-01-05T17:08:40.369126Z",
287+
"level": "info"
288+
}
289+
```
290+
291+
Tool failure (ERROR level):
292+
293+
```json
294+
{
295+
"event": "tool_failed",
296+
"tool_name": "my_tool",
297+
"duration_ms": 12.34,
298+
"error": "Invalid request parameters",
299+
"error_type": "ValueError",
300+
"success": false,
301+
"timestamp": "2025-01-05T17:08:40.135796Z",
302+
"level": "error"
303+
}
304+
```
305+
306+
**Best Practices:**
307+
308+
- Place `@observe_tool` before `@mcp.tool()` decorator
309+
- Use with `@unpack_pydantic_params` for parameter unpacking
310+
- Context parameters (ctx, context) are automatically filtered from logs
311+
- No need to add manual timing or error logging in tool implementation
312+
313+
**Example with Parameter Unpacking:**
314+
315+
```python
316+
@observe_tool
317+
@unpack_pydantic_params
318+
async def search_items(
319+
request: Annotated[SearchItemsRequest, Unpack()], context: Context
320+
) -> SearchItemsResponse:
321+
"""Search for items - automatically instrumented."""
322+
services = get_services(context)
323+
return await services.client.variants.search(request.query)
324+
```
325+
326+
### @observe_service
327+
328+
Instruments service layer methods with debug-level logging.
329+
330+
**Features:**
331+
332+
- Service operation tracking
333+
- Method timing
334+
- Error logging with context
335+
- Class name extraction
336+
337+
**Usage:**
338+
339+
```python
340+
from katana_mcp.logging import observe_service
341+
342+
class MyService:
343+
@observe_service("get_item")
344+
async def get(self, item_id: int) -> Item:
345+
"""Fetch item by ID."""
346+
return await self._fetch(item_id)
347+
348+
@observe_service("create_item")
349+
async def create(self, data: dict) -> Item:
350+
"""Create new item."""
351+
return await self._create(data)
352+
```
353+
354+
**Log Events Produced:**
355+
356+
Operation started (DEBUG level):
357+
358+
```json
359+
{
360+
"event": "service_operation_started",
361+
"service": "MyService",
362+
"operation": "get_item",
363+
"params": {"item_id": 123},
364+
"timestamp": "2025-01-05T17:08:40.123456Z",
365+
"level": "debug"
366+
}
367+
```
368+
369+
Operation completed (DEBUG level):
370+
371+
```json
372+
{
373+
"event": "service_operation_completed",
374+
"service": "MyService",
375+
"operation": "get_item",
376+
"duration_ms": 45.67,
377+
"success": true,
378+
"timestamp": "2025-01-05T17:08:40.169126Z",
379+
"level": "debug"
380+
}
381+
```
382+
383+
Operation failed (ERROR level):
384+
385+
```json
386+
{
387+
"event": "service_operation_failed",
388+
"service": "MyService",
389+
"operation": "get_item",
390+
"duration_ms": 12.34,
391+
"error": "Item not found",
392+
"error_type": "ItemNotFoundError",
393+
"success": false,
394+
"timestamp": "2025-01-05T17:08:40.135796Z",
395+
"level": "error"
396+
}
397+
```
398+
399+
**Best Practices:**
400+
401+
- Use descriptive operation names (e.g., "get_product", "create_order")
402+
- Service operations log at DEBUG level (less verbose than tools)
403+
- Use for lower-level operations called by tools
404+
- Provides granular visibility into service layer performance
405+
406+
### Decorator Comparison
407+
408+
| Feature | @observe_tool | @observe_service |
409+
| ----------------------- | ---------------------------------- | ------------------------------------- |
410+
| **Use Case** | MCP tools (user-facing operations) | Service methods (internal operations) |
411+
| **Log Level** | INFO | DEBUG |
412+
| **Parameter Filtering** | Yes (ctx/context) | No |
413+
| **Primary Audience** | Operations/users | Developers |
414+
| **Visibility** | High (always logged in production) | Low (only in debug mode) |
415+
416+
### Migration Guide
417+
418+
**Before (Manual Logging):**
419+
420+
```python
421+
@mcp.tool()
422+
async def my_tool(request: MyRequest, context: Context) -> MyResponse:
423+
logger.info("my_tool_started", request=request)
424+
start_time = time.monotonic()
425+
try:
426+
result = await do_work()
427+
duration_ms = (time.monotonic() - start_time) * 1000
428+
logger.info("my_tool_completed", duration_ms=duration_ms)
429+
return result
430+
except Exception as e:
431+
logger.error("my_tool_failed", error=str(e))
432+
raise
433+
```
434+
435+
**After (With Decorator):**
436+
437+
```python
438+
@observe_tool
439+
@mcp.tool()
440+
async def my_tool(request: MyRequest, context: Context) -> MyResponse:
441+
return await do_work() # Automatic logging, timing, error tracking!
442+
```
443+
444+
**Benefits:**
445+
446+
- 10+ lines of boilerplate eliminated per tool
447+
- Consistent log format across all tools
448+
- Automatic error tracking without try/except
449+
- Performance metrics captured automatically
450+
- No risk of forgetting to log important events
451+
213452
## Best Practices
214453

215454
1. **Use INFO level in production** - Provides operational visibility without noise
216-
2. **Use DEBUG for troubleshooting** - Includes detailed execution traces
217-
3. **Use JSON format for log aggregation** - Easier to parse and analyze
218-
4. **Use text format for development** - More human-readable
219-
5. **Monitor duration_ms** - Identify performance bottlenecks
220-
6. **Never log sensitive data** - The filter catches common patterns, but be careful with custom fields
455+
1. **Use DEBUG for troubleshooting** - Includes detailed execution traces and service
456+
operations
457+
1. **Use JSON format for log aggregation** - Easier to parse and analyze
458+
1. **Use text format for development** - More human-readable
459+
1. **Monitor duration_ms** - Identify performance bottlenecks
460+
1. **Never log sensitive data** - The filter catches common patterns, but be careful
461+
with custom fields
462+
1. **Use @observe_tool for all MCP tools** - Automatic instrumentation with zero
463+
boilerplate
464+
1. **Use @observe_service for service methods** - Granular visibility into internal
465+
operations
221466

222467
## References
223468

0 commit comments

Comments
 (0)