Files
breakpilot-lehrer/admin-lehrer/app/api/infrastructure/log-extract/extract/route.ts
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

396 lines
10 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import type { ExtractedError, ErrorCategory, LogExtractionResponse } from '@/types/infrastructure-modules'
// Woodpecker API configuration
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
// =============================================================================
// Error Pattern Matching
// =============================================================================
interface ErrorPattern {
pattern: RegExp
category: ErrorCategory
extractMessage?: (match: RegExpMatchArray, line: string) => string
}
/**
* Patterns fuer verschiedene Fehlertypen in CI/CD Logs
*/
const ERROR_PATTERNS: ErrorPattern[] = [
// Test Failures
{
pattern: /^(FAIL|FAILED|ERROR):?\s+(.+)$/i,
category: 'test_failure',
extractMessage: (match, line) => match[2] || line,
},
{
pattern: /^---\s+FAIL:\s+(.+)\s+\([\d.]+s\)$/,
category: 'test_failure',
extractMessage: (match) => `Test failed: ${match[1]}`,
},
{
pattern: /pytest.*FAILED\s+(.+)$/,
category: 'test_failure',
extractMessage: (match) => `pytest: ${match[1]}`,
},
{
pattern: /AssertionError:\s+(.+)$/,
category: 'test_failure',
extractMessage: (match) => `Assertion failed: ${match[1]}`,
},
{
pattern: /FAIL\s+[\w\/]+\s+\[build failed\]/,
category: 'build_error',
},
// Build Errors
{
pattern: /^(error|Error)\[[\w-]+\]:\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => match[2],
},
{
pattern: /cannot find (module|package)\s+["'](.+)["']/i,
category: 'build_error',
extractMessage: (match) => `Missing ${match[1]}: ${match[2]}`,
},
{
pattern: /undefined:\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => `Undefined: ${match[1]}`,
},
{
pattern: /compilation failed/i,
category: 'build_error',
},
{
pattern: /npm ERR!\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => `npm error: ${match[1]}`,
},
{
pattern: /go:\s+(.+):\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => `Go: ${match[1]}: ${match[2]}`,
},
// Security Warnings
{
pattern: /\[CRITICAL\]\s+(.+)$/i,
category: 'security_warning',
extractMessage: (match) => `Critical: ${match[1]}`,
},
{
pattern: /\[HIGH\]\s+(.+)$/i,
category: 'security_warning',
extractMessage: (match) => `High severity: ${match[1]}`,
},
{
pattern: /CVE-\d{4}-\d+/,
category: 'security_warning',
extractMessage: (match, line) => line.trim(),
},
{
pattern: /vulnerability found/i,
category: 'security_warning',
},
{
pattern: /secret.*detected/i,
category: 'security_warning',
},
{
pattern: /gitleaks.*found/i,
category: 'security_warning',
},
{
pattern: /semgrep.*finding/i,
category: 'security_warning',
},
// License Violations
{
pattern: /license.*violation/i,
category: 'license_violation',
},
{
pattern: /incompatible license/i,
category: 'license_violation',
},
{
pattern: /AGPL|GPL-3|SSPL/,
category: 'license_violation',
extractMessage: (match, line) => `Potentially problematic license found: ${match[0]}`,
},
// Dependency Issues
{
pattern: /dependency.*not found/i,
category: 'dependency_issue',
},
{
pattern: /outdated.*dependency/i,
category: 'dependency_issue',
},
{
pattern: /version conflict/i,
category: 'dependency_issue',
},
]
/**
* Patterns to extract file paths from error lines
*/
const FILE_PATH_PATTERNS = [
/([\/\w.-]+\.(go|py|ts|tsx|js|jsx|rs)):(\d+)/,
/File "([^"]+)", line (\d+)/,
/at ([\/\w.-]+):(\d+):\d+/,
]
/**
* Patterns to extract service names from log lines or paths
*/
const SERVICE_PATTERNS = [
/service[s]?\/([a-z-]+)/i,
/\/([a-z-]+-service)\//i,
/^([a-z-]+):\s/,
]
// =============================================================================
// Log Parsing Functions
// =============================================================================
interface LogLine {
pos: number
out: string
time: number
}
function extractFilePath(line: string): { path?: string; lineNumber?: number } {
for (const pattern of FILE_PATH_PATTERNS) {
const match = line.match(pattern)
if (match) {
return {
path: match[1],
lineNumber: parseInt(match[2] || match[3], 10) || undefined,
}
}
}
return {}
}
function extractService(line: string, filePath?: string): string | undefined {
// First try to extract from file path
if (filePath) {
for (const pattern of SERVICE_PATTERNS) {
const match = filePath.match(pattern)
if (match) return match[1]
}
}
// Then try from the line itself
for (const pattern of SERVICE_PATTERNS) {
const match = line.match(pattern)
if (match) return match[1]
}
return undefined
}
function parseLogLines(logs: LogLine[], stepName: string): ExtractedError[] {
const errors: ExtractedError[] = []
const seenMessages = new Set<string>()
for (const logLine of logs) {
const line = logLine.out.trim()
if (!line) continue
for (const errorPattern of ERROR_PATTERNS) {
const match = line.match(errorPattern.pattern)
if (match) {
const message = errorPattern.extractMessage
? errorPattern.extractMessage(match, line)
: line
// Deduplicate similar errors
const messageKey = `${errorPattern.category}:${message.substring(0, 100)}`
if (seenMessages.has(messageKey)) continue
seenMessages.add(messageKey)
const fileInfo = extractFilePath(line)
const service = extractService(line, fileInfo.path)
errors.push({
step: stepName,
line: logLine.pos,
message,
category: errorPattern.category,
file_path: fileInfo.path,
service,
})
break // Only match first pattern per line
}
}
}
return errors
}
// =============================================================================
// API Handler
// =============================================================================
/**
* POST /api/infrastructure/logs/extract
*
* Extrahiert Fehler aus Woodpecker Pipeline Logs.
*
* Request Body:
* - pipeline_number: number (required)
* - repo_id?: string (default: '1')
*
* Response:
* - errors: ExtractedError[]
* - pipeline_number: number
* - extracted_at: string
* - lines_parsed: number
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { pipeline_number, repo_id = '1' } = body
if (!pipeline_number) {
return NextResponse.json(
{ error: 'pipeline_number ist erforderlich' },
{ status: 400 }
)
}
// 1. Fetch pipeline details to get step IDs
const pipelineResponse = await fetch(
`${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
}
)
if (!pipelineResponse.ok) {
return NextResponse.json(
{ error: `Pipeline ${pipeline_number} nicht gefunden` },
{ status: 404 }
)
}
const pipeline = await pipelineResponse.json()
// 2. Extract step IDs from workflows
const failedSteps: { id: number; name: string }[] = []
if (pipeline.workflows) {
for (const workflow of pipeline.workflows) {
if (workflow.children) {
for (const child of workflow.children) {
if (child.state === 'failure' || child.state === 'error') {
failedSteps.push({
id: child.id,
name: child.name,
})
}
}
}
}
}
// 3. Fetch logs for each failed step
const allErrors: ExtractedError[] = []
let totalLinesParsed = 0
for (const step of failedSteps) {
try {
const logsResponse = await fetch(
`${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}/logs/${step.id}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
}
)
if (logsResponse.ok) {
const logs: LogLine[] = await logsResponse.json()
totalLinesParsed += logs.length
const stepErrors = parseLogLines(logs, step.name)
allErrors.push(...stepErrors)
}
} catch (logError) {
console.error(`Failed to fetch logs for step ${step.name}:`, logError)
}
}
// 4. Sort errors by severity (security > license > build > test > dependency)
const categoryPriority: Record<ErrorCategory, number> = {
'security_warning': 1,
'license_violation': 2,
'build_error': 3,
'test_failure': 4,
'dependency_issue': 5,
}
allErrors.sort((a, b) => categoryPriority[a.category] - categoryPriority[b.category])
const response: LogExtractionResponse = {
errors: allErrors,
pipeline_number,
extracted_at: new Date().toISOString(),
lines_parsed: totalLinesParsed,
}
return NextResponse.json(response)
} catch (error) {
console.error('Log extraction error:', error)
return NextResponse.json(
{ error: 'Fehler bei der Log-Extraktion' },
{ status: 500 }
)
}
}
/**
* GET /api/infrastructure/logs/extract?pipeline_number=123
*
* Convenience method - calls POST internally
*/
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const pipeline_number = searchParams.get('pipeline_number')
const repo_id = searchParams.get('repo_id') || '1'
if (!pipeline_number) {
return NextResponse.json(
{ error: 'pipeline_number Query-Parameter ist erforderlich' },
{ status: 400 }
)
}
// Create a mock request with JSON body
const mockRequest = new NextRequest(request.url, {
method: 'POST',
body: JSON.stringify({ pipeline_number: parseInt(pipeline_number, 10), repo_id }),
headers: {
'Content-Type': 'application/json',
},
})
return POST(mockRequest)
}