Skip to content

Route dédiée Logs pour Agent Vigee

Cette documentation explique comment configurer une route d'accès aux logs dans vos projets clients pour permettre à l'agent de résolution automatique Vigee d'analyser les erreurs de production.

Pourquoi cette route ?

L'agent Vigee peut résoudre automatiquement les tickets de support en analysant :

  • Le code source (via GitHub)
  • Les logs de production (via cette route)

Sans accès aux logs, l'agent ne peut pas diagnostiquer les erreurs runtime, les stack traces, ou les problèmes de configuration.

Sécurité

Important

Cette route expose des informations sensibles. Elle doit être :

  • Protégée par une clé API unique
  • Limitée en volume de données retournées
  • Filtrée pour exclure les données sensibles (passwords, tokens, etc.)

Implémentation Laravel

1. Créer le Controller

php
<?php
// app/Http/Controllers/Api/VigeeLogController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\File;
use Carbon\Carbon;

class VigeeLogController extends Controller
{
    /**
     * Retourne les erreurs récentes des logs Laravel
     *
     * @param Request $request
     * @return JsonResponse
     */
    public function getErrors(Request $request): JsonResponse
    {
        // Paramètres avec valeurs par défaut sécurisées
        $hours = min($request->get('hours', 24), 72); // Max 72h
        $limit = min($request->get('limit', 100), 500); // Max 500 entrées
        $level = $request->get('level', 'error'); // error, warning, info

        $logPath = storage_path('logs/laravel.log');

        if (!File::exists($logPath)) {
            return response()->json([
                'errors' => [],
                'message' => 'No log file found'
            ]);
        }

        $errors = $this->parseLogFile($logPath, $hours, $limit, $level);

        return response()->json([
            'errors' => $errors,
            'count' => count($errors),
            'period_hours' => $hours,
            'generated_at' => now()->toISOString()
        ]);
    }

    /**
     * Parse le fichier de log et extrait les erreurs récentes
     */
    private function parseLogFile(string $path, int $hours, int $limit, string $level): array
    {
        $content = File::get($path);
        $errors = [];
        $cutoffTime = Carbon::now()->subHours($hours);

        // Pattern pour les entrées de log Laravel
        $pattern = '/\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*)\]\s+\w+\.(\w+):\s+(.*?)(?=\[\d{4}-\d{2}-\d{2}|\z)/s';

        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);

        foreach ($matches as $match) {
            try {
                $timestamp = Carbon::parse($match[1]);
                $logLevel = strtolower($match[2]);
                $message = trim($match[3]);

                // Filtrer par date
                if ($timestamp->lt($cutoffTime)) {
                    continue;
                }

                // Filtrer par niveau
                if (!$this->matchesLevel($logLevel, $level)) {
                    continue;
                }

                // Nettoyer les données sensibles
                $message = $this->sanitizeMessage($message);

                $errors[] = [
                    'timestamp' => $timestamp->toISOString(),
                    'level' => $logLevel,
                    'message' => $this->truncateMessage($message, 2000),
                ];

                if (count($errors) >= $limit) {
                    break;
                }
            } catch (\Exception $e) {
                continue;
            }
        }

        // Trier par date décroissante (plus récent en premier)
        usort($errors, fn($a, $b) => $b['timestamp'] <=> $a['timestamp']);

        return $errors;
    }

    /**
     * Vérifie si le niveau de log correspond au filtre
     */
    private function matchesLevel(string $logLevel, string $filterLevel): bool
    {
        $levels = ['debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'];
        $filterIndex = array_search($filterLevel, $levels);
        $logIndex = array_search($logLevel, $levels);

        return $logIndex !== false && $filterIndex !== false && $logIndex >= $filterIndex;
    }

    /**
     * Supprime les données sensibles du message
     */
    private function sanitizeMessage(string $message): string
    {
        // Masquer les passwords
        $message = preg_replace('/password["\']?\s*[=:]\s*["\']?[^"\'\s,}]+/i', 'password=***REDACTED***', $message);

        // Masquer les tokens
        $message = preg_replace('/token["\']?\s*[=:]\s*["\']?[^"\'\s,}]+/i', 'token=***REDACTED***', $message);

        // Masquer les clés API
        $message = preg_replace('/api[_-]?key["\']?\s*[=:]\s*["\']?[^"\'\s,}]+/i', 'api_key=***REDACTED***', $message);

        // Masquer les secrets
        $message = preg_replace('/secret["\']?\s*[=:]\s*["\']?[^"\'\s,}]+/i', 'secret=***REDACTED***', $message);

        // Masquer les Bearer tokens
        $message = preg_replace('/Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/i', 'Bearer ***REDACTED***', $message);

        return $message;
    }

    /**
     * Tronque le message si trop long
     */
    private function truncateMessage(string $message, int $maxLength): string
    {
        if (strlen($message) <= $maxLength) {
            return $message;
        }

        return substr($message, 0, $maxLength) . '... [TRUNCATED]';
    }
}

2. Créer le Middleware d'authentification

php
<?php
// app/Http/Middleware/VigeeAgentAuth.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class VigeeAgentAuth
{
    public function handle(Request $request, Closure $next): Response
    {
        $apiKey = $request->header('X-Vigee-Api-Key');
        $expectedKey = config('services.vigee.logs_api_key');

        if (!$expectedKey) {
            return response()->json([
                'error' => 'Vigee logs API not configured'
            ], 503);
        }

        if (!$apiKey || !hash_equals($expectedKey, $apiKey)) {
            return response()->json([
                'error' => 'Unauthorized'
            ], 401);
        }

        return $next($request);
    }
}

3. Enregistrer le Middleware

php
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'vigee.agent' => \App\Http\Middleware\VigeeAgentAuth::class,
    ]);
})

// OU app/Http/Kernel.php (Laravel 10 et moins)
protected $middlewareAliases = [
    // ...
    'vigee.agent' => \App\Http\Middleware\VigeeAgentAuth::class,
];

4. Ajouter la Route

php
// routes/api.php

use App\Http\Controllers\Api\VigeeLogController;

Route::middleware('vigee.agent')
    ->prefix('vigee')
    ->group(function () {
        Route::get('/logs', [VigeeLogController::class, 'getErrors']);
    });

5. Configuration

php
// config/services.php

return [
    // ...

    'vigee' => [
        'logs_api_key' => env('VIGEE_LOGS_API_KEY'),
    ],
];
env
# .env

# Générer avec: openssl rand -hex 32
VIGEE_LOGS_API_KEY=votre_cle_api_secrete_ici

Implémentation Next.js (si logs custom)

Pour les projets Next.js avec système de logs custom (pas Vercel) :

typescript
// app/api/vigee/logs/route.ts

import { NextRequest, NextResponse } from 'next/server';
import fs from 'fs/promises';
import path from 'path';

export async function GET(request: NextRequest) {
  // Vérifier la clé API
  const apiKey = request.headers.get('X-Vigee-Api-Key');

  if (apiKey !== process.env.VIGEE_LOGS_API_KEY) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const { searchParams } = new URL(request.url);
  const hours = Math.min(parseInt(searchParams.get('hours') || '24'), 72);
  const limit = Math.min(parseInt(searchParams.get('limit') || '100'), 500);

  try {
    const logPath = path.join(process.cwd(), 'logs', 'error.log');
    const content = await fs.readFile(logPath, 'utf-8');

    // Parser et filtrer les logs...
    const errors = parseLogContent(content, hours, limit);

    return NextResponse.json({
      errors,
      count: errors.length,
      period_hours: hours,
      generated_at: new Date().toISOString(),
    });
  } catch (error) {
    return NextResponse.json({
      errors: [],
      message: 'No log file found or error reading logs',
    });
  }
}

function parseLogContent(content: string, hours: number, limit: number) {
  // Implémenter le parsing selon votre format de logs
  return [];
}

Configuration côté Vigee Flow

Une fois la route configurée dans votre projet client, renseignez les informations dans Vigee Flow :

  1. Aller dans Projets > Sélectionner le projet
  2. Onglet Paramètres ou Intégrations
  3. Section Agent de résolution
  4. Remplir :
    • URL des logs : https://api.votreprojet.com/api/vigee/logs
    • Clé API : La clé générée avec openssl rand -hex 32

Test de la route

bash
# Tester la route
curl -X GET "https://api.votreprojet.com/api/vigee/logs?hours=24&limit=50" \
  -H "X-Vigee-Api-Key: votre_cle_api"

# Réponse attendue
{
  "errors": [
    {
      "timestamp": "2024-01-15T10:30:00.000Z",
      "level": "error",
      "message": "SQLSTATE[23000]: Integrity constraint violation..."
    }
  ],
  "count": 1,
  "period_hours": 24,
  "generated_at": "2024-01-15T12:00:00.000Z"
}

Paramètres de requête

ParamètreTypeDéfautMaxDescription
hoursint2472Période de temps à analyser
limitint100500Nombre max d'entrées
levelstringerror-Niveau minimum (error, warning, info)

Support des logs Daily

Configuration LOG_STACK=daily

Si votre projet Laravel utilise LOG_STACK=daily (fichiers laravel-YYYY-MM-DD.log), adaptez le controller pour rechercher dans plusieurs fichiers :

php
/**
 * Récupère les fichiers de log pertinents (daily + standard)
 */
private function getLogFiles(int $hours): array
{
    $logPath = storage_path('logs');
    $files = [];

    // Fichier standard laravel.log
    $standardLog = $logPath.'/laravel.log';
    if (file_exists($standardLog)) {
        $files[] = $standardLog;
    }

    // Fichiers daily (laravel-YYYY-MM-DD.log)
    $daysToCheck = (int) ceil($hours / 24) + 1;
    for ($i = 0; $i < $daysToCheck; $i++) {
        $date = Carbon::now()->subDays($i)->format('Y-m-d');
        $dailyLog = $logPath.'/laravel-'.$date.'.log';
        if (file_exists($dailyLog)) {
            $files[] = $dailyLog;
        }
    }

    return array_unique($files);
}

Puis dans getErrors(), itérez sur les fichiers :

php
$logFiles = $this->getLogFiles($hours);
$errors = [];

foreach ($logFiles as $logFile) {
    $fileErrors = $this->parseLogFile($logFile, $hours, $limit - count($errors), $level);
    $errors = array_merge($errors, $fileErrors);
    if (count($errors) >= $limit) {
        break;
    }
}

Bonnes pratiques

Sécurité

  • Utilisez une clé API unique par projet (pas la même partout)
  • Limitez les IP autorisées si possible (whitelist GitHub Actions)
  • Activez le rate limiting sur cette route
  • Loggez les accès à cette route pour audit

Performance

  • La route doit répondre en < 5 secondes
  • Ne lisez pas des fichiers de logs > 100MB
  • Implémentez un cache si les logs sont volumineux

Données sensibles

Le controller fourni masque automatiquement :

  • Passwords
  • Tokens (Bearer, API keys)
  • Secrets

Ajoutez d'autres patterns selon vos besoins métier.