# Step 09: Implement Monitoring & Logging

## Prerequisites
- Step 08 completed (production launcher system working)
- Production launcher with basic monitoring
- Health check system operational

## Objectives
1. Implement comprehensive monitoring system
2. Add structured logging with Storm Logger integration
3. Create alerting and notification system
4. Implement metrics collection and reporting
5. Add performance monitoring

## Implementation Details

### Step 1: Create Advanced Monitoring System
Create `_mcp/monitoring/monitor.py`:

```python
"""
Storm Logger MCP Advanced Monitoring System
Comprehensive monitoring with Storm Logger integration
"""

import time
import json
import psutil
import threading
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
from enum import Enum
from pathlib import Path

# Storm Logger integration
import sys
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))

from storm_logger import get_storm_logger, StormLoggerConfig

class AlertLevel(Enum):
    INFO = "info"
    WARNING = "warning"
    ERROR = "error"
    CRITICAL = "critical"

@dataclass
class PerformanceMetrics:
    timestamp: str
    cpu_percent: float
    memory_mb: float
    memory_percent: float
    disk_io_read: int
    disk_io_write: int
    network_bytes_sent: int
    network_bytes_recv: int
    uptime_seconds: float
    active_connections: int
    thread_count: int

@dataclass
class ServerMetrics:
    server_name: str
    tools_count: int
    health_status: str
    response_time_ms: float
    error_count: int
    last_error: Optional[str]
    requests_total: int
    requests_per_minute: float

@dataclass
class Alert:
    level: AlertLevel
    message: str
    timestamp: str
    component: str
    details: Dict[str, Any]
    resolved: bool = False

class MCPMonitor:
    """Advanced MCP server monitoring with Storm Logger integration."""
    
    def __init__(self, config: Dict):
        self.config = config
        self.start_time = datetime.now()
        self.alerts = []
        self.metrics_history = []
        self.server_metrics = {}
        self.running = False
        
        # Initialize Storm Logger for monitoring
        self.logger = get_storm_logger(
            'mcp_monitor',
            config=StormLoggerConfig(
                name="mcp_monitor",
                level="INFO",
                log_dir=self.config.get("log_dir", "_mcp/logs"),
                enable_file=True,
                enable_console=True,
                enable_metrics=True
            )
        )
        
        # Performance baseline
        self.performance_baseline = self._establish_baseline()
        
        # Alert thresholds
        self.thresholds = self.config.get("alert_thresholds", {
            "cpu_percent": 80,
            "memory_mb": 500,
            "memory_percent": 85,
            "response_time_ms": 1000,
            "error_rate": 0.1
        })
        
        self.logger.info("MCP Monitor initialized", extra={
            "component": "monitor",
            "thresholds": self.thresholds
        })
    
    def _establish_baseline(self) -> PerformanceMetrics:
        """Establish performance baseline."""
        cpu_percent = psutil.cpu_percent(interval=1)
        memory = psutil.virtual_memory()
        disk_io = psutil.disk_io_counters()
        network_io = psutil.net_io_counters()
        
        return PerformanceMetrics(
            timestamp=datetime.now().isoformat(),
            cpu_percent=cpu_percent,
            memory_mb=memory.used / 1024 / 1024,
            memory_percent=memory.percent,
            disk_io_read=disk_io.read_bytes if disk_io else 0,
            disk_io_write=disk_io.write_bytes if disk_io else 0,
            network_bytes_sent=network_io.bytes_sent if network_io else 0,
            network_bytes_recv=network_io.bytes_recv if network_io else 0,
            uptime_seconds=0,
            active_connections=len(psutil.net_connections()),
            thread_count=threading.active_count()
        )
    
    def collect_performance_metrics(self) -> PerformanceMetrics:
        """Collect current performance metrics."""
        try:
            cpu_percent = psutil.cpu_percent(interval=0.1)
            memory = psutil.virtual_memory()
            disk_io = psutil.disk_io_counters()
            network_io = psutil.net_io_counters()
            uptime = (datetime.now() - self.start_time).total_seconds()
            
            return PerformanceMetrics(
                timestamp=datetime.now().isoformat(),
                cpu_percent=cpu_percent,
                memory_mb=memory.used / 1024 / 1024,
                memory_percent=memory.percent,
                disk_io_read=disk_io.read_bytes if disk_io else 0,
                disk_io_write=disk_io.write_bytes if disk_io else 0,
                network_bytes_sent=network_io.bytes_sent if network_io else 0,
                network_bytes_recv=network_io.bytes_recv if network_io else 0,
                uptime_seconds=uptime,
                active_connections=len(psutil.net_connections()),
                thread_count=threading.active_count()
            )
        except Exception as e:
            self.logger.error(f"Failed to collect performance metrics: {e}")
            return None
    
    def collect_server_metrics(self, server) -> ServerMetrics:
        """Collect server-specific metrics."""
        try:
            start_time = time.time()
            
            # Run health check
            health_status = "unknown"
            error_count = 0
            last_error = None
            
            try:
                health_tool = None
                for tool in server.tools:
                    if tool.name == "health_check":
                        health_tool = tool
                        break
                
                if health_tool:
                    health_result = health_tool.function()
                    health_status = health_result.get("overall_status", "unknown")
                    
                    # Count errors from health check
                    for server_info in health_result.get("servers", []):
                        if server_info["status"] == "error":
                            error_count += 1
                            if not last_error:
                                last_error = server_info["message"]
                
            except Exception as e:
                health_status = "error"
                error_count = 1
                last_error = str(e)
            
            response_time = (time.time() - start_time) * 1000  # Convert to ms
            
            return ServerMetrics(
                server_name=server.name,
                tools_count=len(server.tools),
                health_status=health_status,
                response_time_ms=response_time,
                error_count=error_count,
                last_error=last_error,
                requests_total=0,  # This would be tracked separately
                requests_per_minute=0.0
            )
            
        except Exception as e:
            self.logger.error(f"Failed to collect server metrics: {e}")
            return None
    
    def check_alerts(self, perf_metrics: PerformanceMetrics, server_metrics: ServerMetrics):
        """Check for alert conditions."""
        alerts = []
        
        # Performance alerts
        if perf_metrics.cpu_percent > self.thresholds["cpu_percent"]:
            alerts.append(Alert(
                level=AlertLevel.WARNING,
                message=f"High CPU usage: {perf_metrics.cpu_percent:.1f}%",
                timestamp=datetime.now().isoformat(),
                component="performance",
                details={"cpu_percent": perf_metrics.cpu_percent}
            ))
        
        if perf_metrics.memory_mb > self.thresholds["memory_mb"]:
            alerts.append(Alert(
                level=AlertLevel.WARNING,
                message=f"High memory usage: {perf_metrics.memory_mb:.1f}MB",
                timestamp=datetime.now().isoformat(),
                component="performance",
                details={"memory_mb": perf_metrics.memory_mb}
            ))
        
        # Server alerts
        if server_metrics.response_time_ms > self.thresholds["response_time_ms"]:
            alerts.append(Alert(
                level=AlertLevel.WARNING,
                message=f"Slow response time: {server_metrics.response_time_ms:.1f}ms",
                timestamp=datetime.now().isoformat(),
                component="server",
                details={"response_time_ms": server_metrics.response_time_ms}
            ))
        
        if server_metrics.health_status == "error":
            alerts.append(Alert(
                level=AlertLevel.ERROR,
                message=f"Server health check failed: {server_metrics.last_error}",
                timestamp=datetime.now().isoformat(),
                component="server",
                details={"health_status": server_metrics.health_status}
            ))
        
        # Log alerts
        for alert in alerts:
            self.logger.warning(f"ALERT: {alert.message}", extra={
                "alert_level": alert.level.value,
                "component": alert.component,
                "details": alert.details
            })
            
            # Track business metrics
            self.logger.track_business_event("mcp_alert", {
                "level": alert.level.value,
                "component": alert.component,
                "message": alert.message
            })
        
        self.alerts.extend(alerts)
        return alerts
    
    def generate_report(self) -> Dict:
        """Generate comprehensive monitoring report."""
        recent_metrics = self.metrics_history[-10:] if self.metrics_history else []
        recent_alerts = [a for a in self.alerts if not a.resolved][-10:]
        
        # Calculate averages
        avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics) if recent_metrics else 0
        avg_memory = sum(m.memory_mb for m in recent_metrics) / len(recent_metrics) if recent_metrics else 0
        
        report = {
            "timestamp": datetime.now().isoformat(),
            "uptime_seconds": (datetime.now() - self.start_time).total_seconds(),
            "status": "healthy" if not recent_alerts else "warning",
            "performance": {
                "avg_cpu_percent": avg_cpu,
                "avg_memory_mb": avg_memory,
                "current_threads": threading.active_count()
            },
            "alerts": {
                "total": len(self.alerts),
                "unresolved": len(recent_alerts),
                "recent": [asdict(a) for a in recent_alerts]
            },
            "servers": dict(self.server_metrics)
        }
        
        # Log report
        self.logger.info("Generated monitoring report", extra={
            "report_summary": {
                "status": report["status"],
                "uptime_hours": report["uptime_seconds"] / 3600,
                "alerts_count": len(recent_alerts)
            }
        })
        
        return report
    
    def start_monitoring(self, server, interval: int = 60):
        """Start monitoring loop."""
        self.running = True
        
        def monitor_loop():
            while self.running:
                try:
                    # Collect metrics
                    perf_metrics = self.collect_performance_metrics()
                    server_metrics = self.collect_server_metrics(server)
                    
                    if perf_metrics and server_metrics:
                        # Store metrics
                        self.metrics_history.append(perf_metrics)
                        self.server_metrics[server_metrics.server_name] = asdict(server_metrics)
                        
                        # Keep only last 100 metrics
                        if len(self.metrics_history) > 100:
                            self.metrics_history = self.metrics_history[-100:]
                        
                        # Check alerts
                        self.check_alerts(perf_metrics, server_metrics)
                        
                        # Log metrics
                        self.logger.info("Metrics collected", extra={
                            "performance": asdict(perf_metrics),
                            "server": asdict(server_metrics)
                        })
                        
                        # Track business metrics
                        self.logger.track_business_event("mcp_metrics", {
                            "cpu_percent": perf_metrics.cpu_percent,
                            "memory_mb": perf_metrics.memory_mb,
                            "health_status": server_metrics.health_status
                        })
                    
                except Exception as e:
                    self.logger.error(f"Monitoring error: {e}")
                
                time.sleep(interval)
        
        monitoring_thread = threading.Thread(target=monitor_loop, daemon=True)
        monitoring_thread.start()
        
        self.logger.info("Monitoring started", extra={
            "interval": interval,
            "server": server.name if server else "unknown"
        })
    
    def stop_monitoring(self):
        """Stop monitoring."""
        self.running = False
        self.logger.info("Monitoring stopped")
    
    def save_report(self, filename: str):
        """Save monitoring report to file."""
        report = self.generate_report()
        
        try:
            Path(filename).parent.mkdir(parents=True, exist_ok=True)
            with open(filename, 'w') as f:
                json.dump(report, f, indent=2)
            
            self.logger.info(f"Report saved to {filename}")
            
        except Exception as e:
            self.logger.error(f"Failed to save report: {e}")
```

### Step 2: Create Notification System
Create `_mcp/monitoring/notifications.py`:

```python
"""
Storm Logger MCP Notification System
Handles alerts and notifications for MCP monitoring
"""

import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Dict, List, Optional
from datetime import datetime
from pathlib import Path

class NotificationManager:
    """Manages notifications for MCP monitoring alerts."""
    
    def __init__(self, config: Dict):
        self.config = config
        self.enabled = config.get("notifications", {}).get("enabled", False)
        self.email_config = config.get("notifications", {}).get("email", {})
        self.webhook_config = config.get("notifications", {}).get("webhook", {})
        
    def send_alert(self, alert: Dict):
        """Send alert notification."""
        if not self.enabled:
            return
        
        # Email notification
        if self.email_config.get("enabled", False):
            self._send_email_alert(alert)
        
        # Webhook notification
        if self.webhook_config.get("enabled", False):
            self._send_webhook_alert(alert)
        
        # Log notification
        self._log_notification(alert)
    
    def _send_email_alert(self, alert: Dict):
        """Send email alert."""
        try:
            msg = MIMEMultipart()
            msg['From'] = self.email_config['from']
            msg['To'] = ', '.join(self.email_config['to'])
            msg['Subject'] = f"Storm MCP Alert: {alert['level'].upper()}"
            
            body = f"""
Storm Logger MCP Alert

Level: {alert['level'].upper()}
Component: {alert['component']}
Message: {alert['message']}
Timestamp: {alert['timestamp']}

Details:
{json.dumps(alert['details'], indent=2)}

--
Storm Logger MCP Monitoring System
"""
            
            msg.attach(MIMEText(body, 'plain'))
            
            server = smtplib.SMTP(self.email_config['smtp_server'], self.email_config['smtp_port'])
            if self.email_config.get('use_tls', False):
                server.starttls()
            if self.email_config.get('username'):
                server.login(self.email_config['username'], self.email_config['password'])
            
            server.send_message(msg)
            server.quit()
            
        except Exception as e:
            print(f"Failed to send email alert: {e}")
    
    def _send_webhook_alert(self, alert: Dict):
        """Send webhook alert."""
        try:
            import requests
            
            payload = {
                "timestamp": alert['timestamp'],
                "level": alert['level'],
                "component": alert['component'],
                "message": alert['message'],
                "details": alert['details']
            }
            
            response = requests.post(
                self.webhook_config['url'],
                json=payload,
                headers=self.webhook_config.get('headers', {}),
                timeout=30
            )
            
            if response.status_code != 200:
                print(f"Webhook alert failed: {response.status_code}")
                
        except Exception as e:
            print(f"Failed to send webhook alert: {e}")
    
    def _log_notification(self, alert: Dict):
        """Log notification to file."""
        try:
            log_file = self.config.get("notifications", {}).get("log_file", "_mcp/logs/notifications.log")
            Path(log_file).parent.mkdir(parents=True, exist_ok=True)
            
            log_entry = {
                "timestamp": datetime.now().isoformat(),
                "alert": alert,
                "notification_sent": True
            }
            
            with open(log_file, 'a') as f:
                f.write(json.dumps(log_entry) + '\n')
                
        except Exception as e:
            print(f"Failed to log notification: {e}")
```

### Step 3: Update Production Launcher with Advanced Monitoring
Update `_mcp/storm_mcp_launcher.py` to include advanced monitoring:

```python
# Add to imports
from .monitoring.monitor import MCPMonitor
from .monitoring.notifications import NotificationManager

# Add to StormMCPLauncher.__init__
self.monitor = None
self.notification_manager = None

# Add to start_server method
def start_server(self):
    # ... existing code ...
    
    # Initialize advanced monitoring
    if self.config.get("advanced_monitoring", {}).get("enabled", False):
        self.monitor = MCPMonitor(self.config.get("advanced_monitoring", {}))
        self.notification_manager = NotificationManager(self.config)
        
        # Start monitoring
        self.monitor.start_monitoring(self.server, interval=self.config.get("monitoring_interval", 60))
        logger.info("Advanced monitoring started")
    
    # ... rest of existing code ...
```

### Step 4: Create Monitoring Dashboard
Create `_mcp/monitoring/dashboard.py`:

```python
"""
Storm Logger MCP Monitoring Dashboard
Web-based dashboard for monitoring MCP servers
"""

import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List

class MonitoringDashboard:
    """Simple web dashboard for MCP monitoring."""
    
    def __init__(self, config: Dict):
        self.config = config
        self.template_dir = Path(__file__).parent / "templates"
        self.static_dir = Path(__file__).parent / "static"
    
    def generate_dashboard_html(self, monitor_data: Dict) -> str:
        """Generate HTML dashboard."""
        template = """
<!DOCTYPE html>
<html>
<head>
    <title>Storm Logger MCP Monitoring Dashboard</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; }
        .metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; }
        .metric-card { background: #f8f9fa; padding: 20px; border-radius: 5px; border-left: 4px solid #3498db; }
        .metric-value { font-size: 2em; font-weight: bold; color: #2c3e50; }
        .metric-label { color: #7f8c8d; }
        .alert { background: #e74c3c; color: white; padding: 10px; border-radius: 5px; margin: 10px 0; }
        .warning { background: #f39c12; }
        .healthy { background: #27ae60; }
        .timestamp { color: #7f8c8d; font-size: 0.9em; }
    </style>
</head>
<body>
    <div class="header">
        <h1>⚡ Storm Logger MCP Monitoring Dashboard</h1>
        <p>Real-time monitoring of MCP server infrastructure</p>
    </div>
    
    <div class="metrics">
        <div class="metric-card">
            <div class="metric-label">System Status</div>
            <div class="metric-value {status_class}">{status}</div>
        </div>
        
        <div class="metric-card">
            <div class="metric-label">Uptime</div>
            <div class="metric-value">{uptime_hours:.1f}h</div>
        </div>
        
        <div class="metric-card">
            <div class="metric-label">CPU Usage</div>
            <div class="metric-value">{cpu_percent:.1f}%</div>
        </div>
        
        <div class="metric-card">
            <div class="metric-label">Memory Usage</div>
            <div class="metric-value">{memory_mb:.1f}MB</div>
        </div>
        
        <div class="metric-card">
            <div class="metric-label">Active Alerts</div>
            <div class="metric-value">{alerts_count}</div>
        </div>
        
        <div class="metric-card">
            <div class="metric-label">Servers</div>
            <div class="metric-value">{servers_count}</div>
        </div>
    </div>
    
    <div class="alerts">
        <h2>Recent Alerts</h2>
        {alerts_html}
    </div>
    
    <div class="servers">
        <h2>Server Status</h2>
        {servers_html}
    </div>
    
    <div class="timestamp">
        Last updated: {timestamp}
    </div>
    
    <script>
        // Auto-refresh every 30 seconds
        setTimeout(() => location.reload(), 30000);
    </script>
</body>
</html>
"""
        
        # Process data
        status = monitor_data.get("status", "unknown")
        status_class = "healthy" if status == "healthy" else "warning"
        uptime_hours = monitor_data.get("uptime_seconds", 0) / 3600
        
        performance = monitor_data.get("performance", {})
        cpu_percent = performance.get("avg_cpu_percent", 0)
        memory_mb = performance.get("avg_memory_mb", 0)
        
        alerts = monitor_data.get("alerts", {})
        alerts_count = alerts.get("unresolved", 0)
        
        servers = monitor_data.get("servers", {})
        servers_count = len(servers)
        
        # Generate alerts HTML
        alerts_html = ""
        for alert in alerts.get("recent", []):
            alert_class = "alert warning" if alert["level"] == "warning" else "alert"
            alerts_html += f'<div class="{alert_class}">{alert["message"]} - {alert["component"]}</div>'
        
        # Generate servers HTML
        servers_html = ""
        for server_name, server_data in servers.items():
            health_class = "healthy" if server_data["health_status"] == "healthy" else "warning"
            servers_html += f"""
            <div class="metric-card">
                <div class="metric-label">{server_name}</div>
                <div class="metric-value {health_class}">{server_data["health_status"]}</div>
                <div class="metric-label">Tools: {server_data["tools_count"]}</div>
            </div>
            """
        
        return template.format(
            status=status.upper(),
            status_class=status_class,
            uptime_hours=uptime_hours,
            cpu_percent=cpu_percent,
            memory_mb=memory_mb,
            alerts_count=alerts_count,
            servers_count=servers_count,
            alerts_html=alerts_html,
            servers_html=servers_html,
            timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        )
    
    def save_dashboard(self, monitor_data: Dict, filename: str):
        """Save dashboard to HTML file."""
        html = self.generate_dashboard_html(monitor_data)
        
        try:
            Path(filename).parent.mkdir(parents=True, exist_ok=True)
            with open(filename, 'w') as f:
                f.write(html)
        except Exception as e:
            print(f"Failed to save dashboard: {e}")
```

## Testing/Validation

### Step 1: Test Advanced Monitoring
```bash
# Test monitoring system
python -c "
import sys
sys.path.append('.')
from _mcp.monitoring.monitor import MCPMonitor
from _mcp.main import create_storm_mcp_server

config = {'log_dir': '_mcp/logs', 'alert_thresholds': {'cpu_percent': 80}}
monitor = MCPMonitor(config)
server = create_storm_mcp_server()

# Test metrics collection
perf_metrics = monitor.collect_performance_metrics()
server_metrics = monitor.collect_server_metrics(server)

print(f'Performance metrics: {perf_metrics.cpu_percent}% CPU')
print(f'Server metrics: {server_metrics.health_status}')
"
```

### Step 2: Test Notification System
```bash
# Test notifications
python -c "
import sys
sys.path.append('.')
from _mcp.monitoring.notifications import NotificationManager

config = {'notifications': {'enabled': True, 'log_file': '_mcp/logs/test_notifications.log'}}
notifier = NotificationManager(config)

alert = {
    'level': 'warning',
    'component': 'test',
    'message': 'Test alert',
    'timestamp': '2025-01-18T16:30:00Z',
    'details': {'test': True}
}

notifier.send_alert(alert)
print('Test notification sent')
"
```

### Step 3: Test Dashboard Generation
```bash
# Test dashboard
python -c "
import sys
sys.path.append('.')
from _mcp.monitoring.dashboard import MonitoringDashboard

config = {}
dashboard = MonitoringDashboard(config)

test_data = {
    'status': 'healthy',
    'uptime_seconds': 3600,
    'performance': {'avg_cpu_percent': 25.5, 'avg_memory_mb': 128.0},
    'alerts': {'unresolved': 0, 'recent': []},
    'servers': {'test_server': {'health_status': 'healthy', 'tools_count': 5}}
}

dashboard.save_dashboard(test_data, '_mcp/logs/test_dashboard.html')
print('Test dashboard saved')
"
```

## Success Criteria
- [ ] Advanced monitoring system implemented
- [ ] Storm Logger integration working
- [ ] Notification system functional
- [ ] Dashboard generation working
- [ ] Performance metrics collection
- [ ] Alert system operational

## JSON Update Instructions

When starting this step:
```json
{
  "steps": {
    "09": {
      "status": "in_progress",
      "progress": 25,
      "issues": ["Implementing advanced monitoring"],
      "next_action": "Create monitoring system with Storm Logger integration"
    }
  }
}
```

When 50% complete (monitoring implemented):
```json
{
  "steps": {
    "09": {
      "status": "in_progress",
      "progress": 50,
      "issues": ["Implementing notifications and dashboard"],
      "next_action": "Create notification system and dashboard"
    }
  }
}
```

When complete:
```json
{
  "steps": {
    "09": {
      "status": "completed",
      "progress": 100,
      "issues": [],
      "next_action": "Proceed to Step 10"
    }
  },
  "current_step": "10",
  "overall_progress": 90,
  "completed_features": [
    "All 7 server modules implemented",
    "Health check functions added",
    "Server creation working",
    "Import dependencies resolved",
    "Complete health check system",
    "Individual server testing complete",
    "MCP integration with test runner",
    "Production launcher system",
    "Advanced monitoring and logging"
  ]
}
```

## Troubleshooting

### Common Issues

1. **Storm Logger integration fails**
   - Check Storm Logger installation
   - Verify configuration is correct
   - Check log directory permissions

2. **Metrics collection fails**
   - Check psutil installation
   - Verify system access permissions
   - Check for missing dependencies

3. **Notifications not working**
   - Check configuration settings
   - Verify email/webhook credentials
   - Check network connectivity

### Debug Commands
```bash
# Test Storm Logger integration
python -c "
from storm_logger import get_storm_logger
logger = get_storm_logger('test')
logger.info('Test message')
print('Storm Logger working')
"

# Test psutil
python -c "
import psutil
print(f'CPU: {psutil.cpu_percent()}%')
print(f'Memory: {psutil.virtual_memory().percent}%')
"
```

## Next Steps
After completing this step:
1. Update JSON status to "completed"
2. Proceed to Step 10: Final Validation & Documentation
3. Use monitoring system for final validation

## Notes
- Monitoring should be comprehensive but not impact performance
- Storm Logger integration provides business intelligence
- Notifications should be configurable and not spam
- Dashboard should be simple but informative