Skip to content

Commit c9d24e8

Browse files
authored
Add Cloud Logger Commands to CLI (#416)
# Add Cloud Logger Commands for Observability ![image.png](https://app.graphite.dev/user-attachments/assets/e25aefcd-3dc5-4425-9e3a-722dd1e655ec.png) This PR adds new commands for configuring logging and retrieving logs from deployed MCP apps: - Adds `mcp-agent cloud logger configure` to set up OTEL endpoints and headers for log collection - Adds `mcp-agent cloud logger tail` to retrieve and stream logs from deployed apps - Supports filtering logs by time duration with `--since` parameter - Implements pattern matching with `--grep` to filter log messages - Adds continuous log streaming with `--follow` flag - Supports sorting logs by timestamp or severity with `--order-by`, `--asc`, and `--desc` options - Provides multiple output formats: text (default), JSON, and YAML - Automatically converts timestamps to local time for better readability - Implements proper log level styling with color-coded output - Handles graceful interruption of log streaming - Adds utility functions for resolving app identifiers and server URLs The implementation includes robust error handling, clear user feedback, and comprehensive help documentation. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Introduced a “cloud logger” CLI subgroup for logging and observability. - Added a “configure” command to set OTEL logging endpoint and headers, with optional connection testing and saved configuration. - Added a “tail” command to fetch or stream app logs with filters and formatting: - Options: --follow, --since, --grep, --limit, --order-by (timestamp|severity), --asc/--desc - Output formats: text, json, yaml (colorized text) - Handles authentication, connectivity, and clear error messages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 182947f commit c9d24e8

File tree

7 files changed

+746
-2
lines changed

7 files changed

+746
-2
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""MCP Agent Cloud Logger commands.
2+
3+
This package contains functionality for configuring observability and retrieving/streaming logs
4+
from deployed MCP apps.
5+
"""
6+
7+
from .configure.main import configure_logger
8+
from .tail.main import tail_logs
9+
10+
__all__ = ["configure_logger", "tail_logs"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Logger configuration command."""
2+
3+
from .main import configure_logger
4+
5+
__all__ = ["configure_logger"]
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""Configure OTEL endpoint and headers for logging."""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
import httpx
7+
import typer
8+
import yaml
9+
from rich.console import Console
10+
from rich.panel import Panel
11+
12+
from mcp_agent.cli.exceptions import CLIError
13+
14+
console = Console()
15+
16+
17+
def configure_logger(
18+
endpoint: Optional[str] = typer.Argument(
19+
None,
20+
help="OTEL endpoint URL for log collection",
21+
),
22+
headers: Optional[str] = typer.Option(
23+
None,
24+
"--headers",
25+
"-h",
26+
help="Additional headers in key=value,key2=value2 format",
27+
),
28+
test: bool = typer.Option(
29+
False,
30+
"--test",
31+
help="Test the connection without saving configuration",
32+
),
33+
) -> None:
34+
"""Configure OTEL endpoint and headers for log collection.
35+
36+
This command allows you to configure the OpenTelemetry endpoint and headers
37+
that will be used for collecting logs from your deployed MCP apps.
38+
39+
Examples:
40+
mcp-agent cloud logger configure https://otel.example.com:4318/v1/logs
41+
mcp-agent cloud logger configure https://otel.example.com --headers "Authorization=Bearer token,X-Custom=value"
42+
mcp-agent cloud logger configure --test # Test current configuration
43+
"""
44+
if not endpoint and not test:
45+
console.print("[red]Error: Must specify endpoint or use --test[/red]")
46+
raise typer.Exit(1)
47+
48+
config_path = _find_config_file()
49+
50+
if test:
51+
if config_path and config_path.exists():
52+
config = _load_config(config_path)
53+
otel_config = config.get("otel", {})
54+
endpoint = otel_config.get("endpoint")
55+
headers_dict = otel_config.get("headers", {})
56+
else:
57+
console.print("[yellow]No configuration file found. Use --endpoint to set up OTEL configuration.[/yellow]")
58+
raise typer.Exit(1)
59+
else:
60+
headers_dict = {}
61+
if headers:
62+
try:
63+
for header_pair in headers.split(","):
64+
key, value = header_pair.strip().split("=", 1)
65+
headers_dict[key.strip()] = value.strip()
66+
except ValueError:
67+
console.print("[red]Error: Headers must be in format 'key=value,key2=value2'[/red]")
68+
raise typer.Exit(1)
69+
70+
if endpoint:
71+
console.print(f"[blue]Testing connection to {endpoint}...[/blue]")
72+
73+
try:
74+
with httpx.Client(timeout=10.0) as client:
75+
response = client.get(
76+
endpoint.replace("/v1/logs", "/health") if "/v1/logs" in endpoint else f"{endpoint}/health",
77+
headers=headers_dict
78+
)
79+
80+
if response.status_code in [200, 404]: # 404 is fine, means endpoint exists
81+
console.print("[green]✓ Connection successful[/green]")
82+
else:
83+
console.print(f"[yellow]⚠ Got status {response.status_code}, but endpoint is reachable[/yellow]")
84+
85+
except httpx.RequestError as e:
86+
console.print(f"[red]✗ Connection failed: {e}[/red]")
87+
if not test:
88+
console.print("[yellow]Configuration will be saved anyway. Check your endpoint URL and network connection.[/yellow]")
89+
90+
if not test:
91+
if not config_path:
92+
config_path = Path.cwd() / "mcp_agent.config.yaml"
93+
94+
config = _load_config(config_path) if config_path.exists() else {}
95+
96+
if "otel" not in config:
97+
config["otel"] = {}
98+
99+
config["otel"]["endpoint"] = endpoint
100+
config["otel"]["headers"] = headers_dict
101+
102+
try:
103+
config_path.parent.mkdir(parents=True, exist_ok=True)
104+
with open(config_path, "w") as f:
105+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
106+
107+
console.print(Panel(
108+
f"[green]✓ OTEL configuration saved to {config_path}[/green]\n\n"
109+
f"Endpoint: {endpoint}\n"
110+
f"Headers: {len(headers_dict)} configured" + (f" ({', '.join(headers_dict.keys())})" if headers_dict else ""),
111+
title="Configuration Saved",
112+
border_style="green"
113+
))
114+
115+
except Exception as e:
116+
console.print(f"[red]Error saving configuration: {e}[/red]")
117+
raise typer.Exit(1)
118+
119+
120+
def _find_config_file() -> Optional[Path]:
121+
"""Find mcp_agent.config.yaml by searching upward from current directory."""
122+
current = Path.cwd()
123+
while current != current.parent:
124+
config_path = current / "mcp_agent.config.yaml"
125+
if config_path.exists():
126+
return config_path
127+
current = current.parent
128+
return None
129+
130+
131+
def _load_config(config_path: Path) -> dict:
132+
"""Load configuration from YAML file."""
133+
try:
134+
with open(config_path, "r") as f:
135+
return yaml.safe_load(f) or {}
136+
except Exception as e:
137+
raise CLIError(f"Failed to load config from {config_path}: {e}")
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Logger tail command."""
2+
3+
from .main import tail_logs
4+
5+
__all__ = ["tail_logs"]

0 commit comments

Comments
 (0)