Featured image of post LLM-Powered VPS Cross-Service Log Correlation & Intelligent Incident Analysis

LLM-Powered VPS Cross-Service Log Correlation & Intelligent Incident Analysis

When multiple services on your VPS fail simultaneously, traditional methods struggle to quickly identify root causes. This article shows how to use Large Language Models (LLM) for cross-service log correlation analysis, automatically identifying failure chains and generating actionable root cause reports.

Introduction

Your VPS runs Nginx, MySQL, Redis, Docker containers, and multiple custom applications. One day, users report slow website access. You log into the server and face:

  • Nginx error logs full of 502 Bad Gateway
  • MySQL slow query logs showing connection pool exhaustion
  • Redis reporting OOM command not allowed
  • Docker container logs filled with connection refused
  • System logs containing Out of memory: Killed process

The traditional troubleshooting approach is to manually correlate logs one by one — check Nginx first, then MySQL, then Redis, and finally system logs. But the problem is: these logs are scattered across different files, different timestamps, and different formats. Manual correlation is nearly impossible to do efficiently.

The emergence of Large Language Models (LLM) has completely changed this situation. LLMs possess powerful semantic understanding capabilities, allowing them to read all related logs simultaneously, understand the causal relationships between them, and generate structured root cause analysis reports.

This article will guide you through building an LLM-powered VPS cross-service log correlation analysis system, achieving the leap from “manual needle in a haystack” to “AI-powered automatic location.”


1. Why Is Cross-Service Log Correlation So Difficult?

1.1 Three Major Bottlenecks of Traditional Methods

BottleneckDescriptionImpact
Log SilosLogs from different services have different formats, storage locations, and collection methodsRequires manually switching between multiple tools
Time SynchronizationTimestamps across distributed services may have deviationsDifficult to accurately reconstruct event timelines
Context MissingA single log entry cannot reflect the complete causal chainProne to misidentifying root causes

1.2 Typical Failure Scenario Analysis

Let’s examine a typical multi-service failure scenario:

User Request → Nginx → PHP-FPM → MySQL
                      ↘ Redis (cache)

When users report “slow page loading,” possible causes include:

  1. MySQL slow queries: A SQL query lacks an index, causing full table scans
  2. Redis memory overflow: Cache invalidation causes大量 requests to hit MySQL directly
  3. PHP-FPM process exhaustion: Concurrent connections exceed the configured limit
  4. Nginx upstream timeout: Backend service response timeout, returning 504
  5. System OOM: Insufficient memory, kernel kills critical processes

Traditional troubleshooting requires:

  • Check Nginx access/error logs → confirm 502/504 errors
  • Check MySQL slow query log → find slow queries
  • Check Redis info → examine memory usage
  • Check PHP-FPM status → check process count
  • Check dmesg/journalctl → check OOM killer

This process typically takes 30 minutes to hours, and heavily relies on the operator’s experience.


2. System Architecture Design

2.1 Overall Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                  LLM Cross-Service Log Correlation System             │
├───────────────┬───────────────┬───────────────┬─────────────────────┤
│  Log Capture  │  Pre-processing│   LLM Analysis │    Output Layer     │
├───────────────┼───────────────┼───────────────┼─────────────────────┤
│ • Promtail    │ • Time align   │ • Log aggregation │ • Root cause rpt │
│ • Filebeat    │ • Format std   │ • Correlation   │ • Fix suggestions  │
│ • Docker log  │ • Context ext  │ • Causal reasoning│ • Auto alerts    │
│   driver      │ • Dedup/compress│ • Pattern match │ • Knowledge base │
└───────┬───────┴───────┬───────┴───────┬───────┴──────────┬──────────┘
        │               │               │                 │
        └───────────────┴───────────────┴─────────────────┘
                              │
                              ▼
                    ┌─────────────────────┐
                    │     Target VPS       │
                    │  Nginx + MySQL +    │
                    │  Redis + App +      │
                    │  System Logs        │
                    └─────────────────────┘

2.2 Core Components

1. Log Capture Layer

The capture layer collects logs from various services on the VPS:

  • Promtail: Collects system logs (/var/log/*) and application logs
  • Docker log driver: Collects container logs
  • MySQL slow log: Exported via Prometheus exporter
  • Redis INFO: Key metrics exported via exporter

2. Pre-processing Layer

The pre-processing layer standardizes raw logs:

  • Time alignment: Unifies all logs to the same timezone, handling clock drift
  • Format standardization: Converts different log formats to unified JSON structure
  • Context extraction: Extracts key information from logs (error codes, request IDs, user IDs, etc.)
  • Deduplication & compression: Removes duplicate logs, compresses similar logs

3. LLM Analysis Layer

The analysis layer is the core of the system, leveraging LLM capabilities for intelligent analysis:

  • Log aggregation: Aggregates related logs by time window and service relationships
  • Correlation analysis: Identifies causal relationships between logs from different services
  • Causal reasoning: Infers root causes based on log patterns and domain knowledge
  • Pattern recognition: Identifies known failure patterns and newly emerging anomalies

4. Output Layer

The output layer presents analysis results in readable formats:

  • Root cause report: Describes failure root causes and impact scope in natural language
  • Fix suggestions: Provides actionable remediation steps
  • Auto alerts: Pushes alerts to DingTalk/Slack/Telegram via Webhook
  • Knowledge retention: Stores analysis results to knowledge base for future reference

3. Implementation Details

3.1 Log Capture Configuration

3.1.1 Promtail Configuration

# promtail-config.yml
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  # System logs
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/*.log
          service: system

  # Nginx logs
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          __path__: /var/log/nginx/*.log
          service: nginx

  # Application logs
  - job_name: application
    static_configs:
      - targets:
          - localhost
        labels:
          job: application
          __path__: /app/logs/*.log
          service: application

3.1.2 Docker Log Collection

# docker-compose.yml - Loki + Promtail
services:
  loki:
    image: grafana/loki:3.0.0
    ports:
      - "3100:3100"
    volumes:
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml

  promtail:
    image: grafana/promtail:3.0.0
    volumes:
      - /var/log:/var/log
      - /run/log:/run/log
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

  # Collect Docker container logs
  dockerd-logs:
    image: grafana/promtail:3.0.0
    volumes:
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock
      - ./promtail-docker.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml

3.2 LLM Analysis Pipeline

3.2.1 Log Aggregation and Context Building

import json
from datetime import datetime, timedelta
from typing import List, Dict, Any

class LogAggregator:
    """Log aggregator: correlates related logs by time window and service relationships"""
    
    def __init__(self, window_minutes=10):
        self.window = timedelta(minutes=window_minutes)
    
    def aggregate(self, logs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        """Aggregate logs by time window"""
        if not logs:
            return []
        
        # Sort by time
        sorted_logs = sorted(logs, key=lambda x: x.get('timestamp', ''))
        
        # Group by time window
        groups = []
        current_group = []
        group_start = None
        
        for log in sorted_logs:
            ts = datetime.fromisoformat(log.get('timestamp', '').replace('Z', '+00:00'))
            
            if group_start is None:
                group_start = ts
                current_group.append(log)
            elif ts - group_start <= self.window:
                current_group.append(log)
            else:
                if current_group:
                    groups.append(current_group)
                current_group = [log]
                group_start = ts
        
        if current_group:
            groups.append(current_group)
        
        return groups

3.2.2 LLM Root Cause Analysis Prompt

ROOT_CAUSE_ANALYSIS_PROMPT = """
You are a professional SRE engineer responsible for analyzing cross-service log correlation failures on VPS.

## Task
Analyze the following log snippets, identify the root cause of the failure, and generate a structured root cause report.

## Input Logs
{logs}

## Service Dependency Relationships
{dependencies}

## Analysis Requirements
1. **Timeline Reconstruction**:梳理关键事件的时间顺序
2. **Causal Chain Analysis**: Identify the complete chain from "trigger → cascading impact → final symptom"
3. **Root Cause Localization**: Find the fundamental cause (not the symptom)
4. **Impact Assessment**: Evaluate the scope and severity of the failure's impact on business
5. **Remediation Recommendations**: Provide specific, actionable repair steps

## Output Format
Please output in JSON format:
{{
  "root_cause": "Concise description of root cause",
  "confidence": 0.0-1.0,
  "timeline": [
    {{"time": "timestamp", "event": "event description", "service": "service name"}}
  ],
  "causal_chain": [
    {{"step": 1, "cause": "cause", "effect": "effect"}}
  ],
  "impact": {{
    "services_affected": ["affected services"],
    "severity": "critical/high/medium/low",
    "description": "impact description"
  }},
  "recommendations": [
    {{"action": "remediation action", "priority": "immediate/short-term/long-term", "details": "detailed explanation"}}
  ],
  "similar_incidents": ["IDs of similar historical incidents"]
}}
"""

3.2.3 Cross-Service Correlation Analysis

import ollama
from typing import List, Dict, Any

class CrossServiceAnalyzer:
    """Cross-service log correlation analyzer"""
    
    def __init__(self, model="qwen2.5:7b"):
        self.model = model
        self.service_deps = {
            "nginx": ["php-fpm", "node"],
            "php-fpm": ["mysql", "redis"],
            "node": ["mysql", "redis", "postgresql"],
            "api-gateway": ["auth-service", "user-service", "order-service"],
        }
    
    def analyze(self, log_groups: List[List[Dict]]) -> Dict[str, Any]:
        """Perform cross-service correlation analysis"""
        
        # Build context
        all_logs = []
        for group in log_groups:
            for log in group:
                all_logs.append({
                    "service": log.get("service", "unknown"),
                    "timestamp": log.get("timestamp", ""),
                    "level": log.get("level", "INFO"),
                    "message": log.get("message", ""),
                    "error_code": log.get("error_code", ""),
                })
        
        # Build dependency relationship text
        deps_text = json.dumps(self.service_deps, ensure_ascii=False, indent=2)
        
        # Call LLM for analysis
        prompt = ROOT_CAUSE_ANALYSIS_PROMPT.format(
            logs=json.dumps(all_logs[-100:], ensure_ascii=False, indent=2),
            dependencies=deps_text
        )
        
        response = ollama.chat(
            model=self.model,
            messages=[{"role": "user", "content": prompt}]
        )
        
        # Parse results
        try:
            result = json.loads(response["message"]["content"])
        except json.JSONDecodeError:
            # If JSON parsing fails, try to extract JSON fragment
            content = response["message"]["content"]
            start = content.find("{")
            end = content.rfind("}")
            if start != -1 and end != -1:
                result = json.loads(content[start:end+1])
            else:
                result = {"root_cause": content, "confidence": 0.5}
        
        return result

3.3 Complete Deployment Solution

3.3.1 docker-compose.yml

version: '3.8'

services:
  # Loki - Log storage
  loki:
    image: grafana/loki:3.0.0
    ports:
      - "3100:3100"
    volumes:
      - loki-data:/loki
      - ./loki-config.yml:/etc/loki/local-config.yaml
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped

  # Promtail - Log collection
  promtail:
    image: grafana/promtail:3.0.0
    volumes:
      - /var/log:/var/log:ro
      - /run/log:/run/log:ro
      - ./promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml
    restart: unless-stopped

  # LLM analysis service
  llm-analyzer:
    build: ./llm-analyzer
    volumes:
      - ./config:/app/config
      - ./knowledge-base:/app/knowledge-base
    environment:
      - OLLAMA_HOST=http://ollama:11434
      - LOKI_URL=http://loki:3100
      - ALERT_WEBHOOK=https://hooks.slack.com/your-webhook
    depends_on:
      - loki
      - ollama
    restart: unless-stopped

  # Ollama - Local LLM
  ollama:
    image: ollama/ollama:latest
    ports:
      - "11434:11434"
    volumes:
      - ollama-data:/root/.ollama
    restart: unless-stopped

  # Grafana - Visualization
  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
    environment:
      - GF_AUTH_ANONYMOUS_ENABLED=true
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    depends_on:
      - loki
    restart: unless-stopped

volumes:
  loki-data:
  ollama-data:
  grafana-data:

3.3.2 Loki Configuration

# loki-config.yml
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9095

common:
  instance_addr: 127.0.0.1
  path_prefix: /tmp/loki
  storage:
    filesystem:
      chunks_directory: /tmp/loki/chunks
      rules_directory: /tmp/loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

ruler:
  alertmanager_url: http://localhost:9093

3.4 Usage Examples

3.4.1 Manual Trigger Analysis

# Query logs from the last 30 minutes
curl -G 'http://localhost:3100/loki/api/v1/query_range' \
  --data-urlencode 'query={service=~"nginx|mysql|redis|php-fpm"}' \
  --data-urlencode 'start=now-30m' \
  --data-urlencode 'end=now' \
  --data-urlencode 'limit=1000'

# Trigger LLM analysis
curl -X POST 'http://localhost:8080/analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "log_query": "{service=~\"nginx|mysql|redis|php-fpm\"}",
    "time_range": "30m",
    "context": "User reports slow page loading"
  }'

3.4.2 LLM Analysis Result Example

{
  "root_cause": "MySQL slow queries caused connection pool exhaustion, cascading to Redis cache invalidation and Nginx 502 errors",
  "confidence": 0.92,
  "timeline": [
    {
      "time": "2026-09-12T10:15:23Z",
      "event": "MySQL started showing slow queries (>2s)",
      "service": "mysql"
    },
    {
      "time": "2026-09-12T10:17:45Z",
      "event": "MySQL connections reached limit (max_connections=151)",
      "service": "mysql"
    },
    {
      "time": "2026-09-12T10:18:02Z",
      "event": "PHP-FPM started waiting for MySQL connections, process count reached 20/20",
      "service": "php-fpm"
    },
    {
      "time": "2026-09-12T10:18:30Z",
      "event": "Redis cache hit rate dropped to 15%,大量 requests hitting MySQL directly",
      "service": "redis"
    },
    {
      "time": "2026-09-12T10:19:15Z",
      "event": "Nginx started returning 502 Bad Gateway",
      "service": "nginx"
    }
  ],
  "causal_chain": [
    {"step": 1, "cause": "A SQL query lacks an index", "effect": "MySQL slow queries increase"},
    {"step": 2, "cause": "Slow queries occupy connection pool", "effect": "New requests cannot obtain database connections"},
    {"step": 3, "cause": "PHP-FPM processes waiting for database connections", "effect": "Process pool exhausted"},
    {"step": 4, "cause": "Cache invalidation causes more database queries", "effect": "MySQL pressure further increases"},
    {"step": 5, "cause": "Backend service unresponsive", "effect": "Nginx returns 502"}
  ],
  "impact": {
    "services_affected": ["mysql", "php-fpm", "redis", "nginx"],
    "severity": "high",
    "description": "Website completely inaccessible, affecting all users"
  },
  "recommendations": [
    {
      "action": "Add missing database index",
      "priority": "immediate",
      "details": "Create index on orders.created_at: CREATE INDEX idx_orders_created ON orders(created_at)"
    },
    {
      "action": "Optimize slow query SQL",
      "priority": "immediate",
      "details": "Check EXPLAIN output, optimize query statements, avoid SELECT *"
    },
    {
      "action": "Adjust PHP-FPM and MySQL connection pool configuration",
      "priority": "short-term",
      "details": "Increase max_connections, adjust php-fpm pm.max_children"
    },
    {
      "action": "Configure Redis cache warm-up strategy",
      "priority": "long-term",
      "details": "Avoid cache stampede, use hot key preloading"
    }
  ],
  "similar_incidents": ["INC-2026-0815-001", "INC-2026-0702-003"]
}

4. Advanced Features

4.1 Knowledge Retention and Reuse

The system stores each analysis result in a knowledge base for future reference:

class KnowledgeBase:
    """Incident knowledge base: stores historical analysis results, supports similar incident matching"""
    
    def __init__(self, db_path="knowledge.db"):
        self.db = sqlite3.connect(db_path)
        self._init_tables()
    
    def _init_tables(self):
        self.db.execute('''
            CREATE TABLE IF NOT EXISTS incidents (
                id INTEGER PRIMARY KEY,
                root_cause TEXT,
                timeline TEXT,
                causal_chain TEXT,
                recommendations TEXT,
                created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        self.db.execute('''
            CREATE VIRTUAL TABLE IF NOT EXISTS incidents_fts 
            USING fts5(root_cause, content='incidents', content_rowid='id')
        ''')
    
    def store(self, analysis_result: Dict):
        """Store analysis result"""
        self.db.execute('''
            INSERT INTO incidents (root_cause, timeline, causal_chain, recommendations)
            VALUES (?, ?, ?, ?)
        ''', (
            analysis_result.get('root_cause', ''),
            json.dumps(analysis_result.get('timeline', [])),
            json.dumps(analysis_result.get('causal_chain', [])),
            json.dumps(analysis_result.get('recommendations', []))
        ))
        self.db.commit()
    
    def find_similar(self, root_cause: str, limit=3) -> List[Dict]:
        """Find similar historical incidents"""
        cursor = self.db.execute('''
            SELECT id, root_cause, created_at
            FROM incidents_fts
            WHERE incidents_fts MATCH ?
            ORDER BY rank
            LIMIT ?
        ''', (root_cause, limit))
        return [{'id': row[0], 'root_cause': row[1], 'date': row[2]} for row in cursor.fetchall()]

4.2 Real-Time Alert Integration

import requests

class AlertNotifier:
    """Alert notification"""
    
    def __init__(self, webhook_url):
        self.webhook_url = webhook_url
    
    def notify(self, analysis_result: Dict):
        """Send alert notification"""
        severity = analysis_result.get('impact', {}).get('severity', 'medium')
        root_cause = analysis_result.get('root_cause', 'Unknown')
        
        # Build message
        message = f"🚨 **VPS Incident Alert**\n\n"
        message += f"**Root Cause**: {root_cause}\n"
        message += f"**Severity**: {severity}\n"
        message += f"**Affected Services**: {', '.join(analysis_result.get('impact', {}).get('services_affected', []))}\n\n"
        
        # Add remediation suggestions
        recs = analysis_result.get('recommendations', [])
        if recs:
            message += "**Remediation Steps**:\n"
            for i, rec in enumerate(recs[:3], 1):
                message += f"{i}. {rec.get('action', '')}\n"
        
        # Send to Webhook
        payload = {
            "text": message,
            "attachments": [{
                "color": "danger" if severity in ["critical", "high"] else "warning",
                "fields": [
                    {"title": "Root Cause", "value": root_cause, "short": False},
                    {"title": "Severity", "value": severity, "short": True},
                    {"title": "Confidence", "value": f"{analysis_result.get('confidence', 0):.1%}", "short": True},
                ]
            }]
        }
        
        requests.post(self.webhook_url, json=payload, timeout=10)

4.3 Scheduled Analysis Tasks

import schedule
import time

class ScheduledAnalyzer:
    """Scheduled analysis tasks"""
    
    def __init__(self, analyzer, notifier, knowledge_base):
        self.analyzer = analyzer
        self.notifier = notifier
        self.kb = knowledge_base
    
    def run_analysis(self):
        """Execute analysis task"""
        # Query anomalous logs from the last 10 minutes
        logs = self.fetch_anomaly_logs(minutes=10)
        
        if not logs:
            return
        
        # Execute LLM analysis
        result = self.analyzer.analyze([logs])
        
        # Store to knowledge base
        self.kb.store(result)
        
        # Send alert (if severity is high)
        severity = result.get('impact', {}).get('severity', 'low')
        if severity in ['critical', 'high']:
            self.notifier.notify(result)
    
    def fetch_anomaly_logs(self, minutes=10) -> List[Dict]:
        """Fetch anomalous logs from Loki"""
        # Query ERROR and CRITICAL level logs
        query = '{level=~"ERROR|CRITICAL"}'
        # ... call Loki API
        pass

5. Performance Optimization

5.1 Log Sampling Strategy

For high-traffic VPS, full log analysis may be too costly. Smart sampling is recommended:

class SmartSampler:
    """Smart log sampler"""
    
    def should_sample(self, log: Dict) -> bool:
        """Decide whether to sample this log"""
        # Always retain error logs
        if log.get('level') in ['ERROR', 'CRITICAL']:
            return True
        
        # Keyword logs always retained
        keywords = ['timeout', 'connection refused', 'out of memory', 'killed']
        message = log.get('message', '').lower()
        if any(kw in message for kw in keywords):
            return True
        
        # Normal logs sampled at 1%
        import random
        return random.random() < 0.01

5.2 Incremental Analysis

Avoid re-analyzing the same logs; use a checkpoint mechanism:

class IncrementalAnalyzer:
    """Incremental analyzer"""
    
    def __init__(self, checkpoint_file="checkpoint.json"):
        self.checkpoint = self._load_checkpoint()
    
    def _load_checkpoint(self):
        try:
            with open(self.checkpoint_file, 'r') as f:
                return json.load(f)
        except FileNotFoundError:
            return {"last_analysis": 0}
    
    def get_new_logs(self, since_timestamp):
        """Fetch new logs since specified timestamp"""
        # Call Loki API to get new logs
        pass
    
    def analyze_incremental(self):
        """Incremental analysis"""
        since = self.checkpoint.get('last_analysis', 0)
        new_logs = self.get_new_logs(since)
        
        if new_logs:
            result = self.analyzer.analyze([new_logs])
            self.checkpoint['last_analysis'] = int(time.time() * 1000)
            self._save_checkpoint()
            return result
        return None

6. Summary and Outlook

6.1 Core Value

By building an LLM-powered cross-service log correlation analysis system, you can achieve:

  1. Fast root cause localization: From hours to minutes
  2. Reduced misjudgment rate: LLM understands context, reducing false positives
  3. Knowledge retention: Historical incidents accumulate automatically, enabling quick troubleshooting even for juniors
  4. Automated response: Critical incidents auto-alert, no need for manual monitoring

6.2 Applicable Scenarios

  • Single VPS multi-service architecture: Nginx + PHP/Node + MySQL + Redis
  • Docker containerized deployment: Cross-container log correlation
  • Microservices architecture: Cross-service call chain analysis
  • Hybrid cloud environments: Unified log analysis across local VPS and cloud services

6.3 Next Steps for Improvement

  • Multi-VPS collaborative analysis: Cross-server log correlation
  • Real-time streaming analysis: Real-time log processing based on Kafka/Flink
  • Adaptive thresholds: LLM automatically learns normal patterns, dynamically adjusts alert thresholds
  • Automated remediation execution: Integration with Ansible/Terraform for automatic fix execution

References

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials