# Vector VRL: Multi-format log parser
# Supports: JSON, Python, logfmt, klog, Go, Java, syslog, nginx, plain text, Traefik access logs
#
# Test with: vector vrl --input test_logs.json --program detect_and_parse.vrl

msg = string!(.message)
.log_format = "unknown"

# ========================================
# FORMAT 1: JSON structured logs
# ========================================
parsed, err = parse_json(msg)
if err == null {
  .log_format = "json"

  # ---- Traefik access logs detection ----
  # Traefik access logs have RequestMethod, RequestPath, DownstreamStatus and an empty "msg" field
  if parsed.RequestMethod != null && parsed.RequestPath != null && parsed.DownstreamStatus != null {
    .log_format = "traefik_access"
    .level = "info"
    status = to_int(parsed.DownstreamStatus) ?? 0
    if status >= 500 {
      .level = "error"
    } else if status >= 400 {
      .level = "warn"
    }
    duration_ns = to_int(parsed.Duration) ?? 0
    duration_ms = duration_ns / 1000000
    .parsed_message = string!(parsed.RequestMethod) + " " + string!(parsed.RequestPath) + " " + to_string(status) + " " + to_string(duration_ms) + "ms"
    .method = parsed.RequestMethod
    .path = parsed.RequestPath
    .status_code = parsed.DownstreamStatus
    .duration_ms = duration_ms
    if parsed.ServiceName != null {
      .logger = parsed.ServiceName
    }
    if parsed.RequestHost != null {
      .request_host = parsed.RequestHost
    }
    if parsed.ClientHost != null {
      .client_ip = parsed.ClientHost
    }
    if parsed.RouterName != null {
      .router = parsed.RouterName
    }
    if parsed.TraceId != null {
      .request_id = parsed.TraceId
    }
  }

  # Extract level (check multiple field names) - skip if already set (e.g. Traefik)
  if .level == null && parsed.level != null {
    .level = parsed.level
  }
  if .level == null && parsed.severity != null {
    .level = parsed.severity
  }
  if .level == null && parsed.log_level != null {
    .level = parsed.log_level
  }
  if .level == null && parsed.lvl != null {
    .level = parsed.lvl
  }
  if .level == null && parsed.loglevel != null {
    .level = parsed.loglevel
  }

  # Extract message - skip if already set (e.g. Traefik)
  if .parsed_message == null && parsed.message != null {
    .parsed_message = parsed.message
  }
  if .parsed_message == null && parsed.msg != null && to_string(parsed.msg) != "" {
    .parsed_message = parsed.msg
  }
  if .parsed_message == null && parsed.text != null {
    .parsed_message = parsed.text
  }

  # Extract logger
  if parsed.logger != null {
    .logger = parsed.logger
  }
  if .logger == null && parsed.name != null {
    .logger = parsed.name
  }
  if .logger == null && parsed.module != null {
    .logger = parsed.module
  }

  # Extract error type
  if parsed.error != null {
    .error_type = parsed.error
  }
  if .error_type == null && parsed.err != null {
    .error_type = parsed.err
  }
  if .error_type == null && parsed.exception != null {
    .error_type = parsed.exception
  }
  if .error_type == null && parsed.exc_type != null {
    .error_type = parsed.exc_type
  }

  # Extract error message
  if parsed.error_message != null {
    .error_message = parsed.error_message
  }
  if .error_message == null && parsed.exc_message != null {
    .error_message = parsed.exc_message
  }

  # Extract traceback
  if parsed.traceback != null {
    .traceback = parsed.traceback
  }
  if .traceback == null && parsed.stack_trace != null {
    .traceback = parsed.stack_trace
  }
  if .traceback == null && parsed.stacktrace != null {
    .traceback = parsed.stacktrace
  }

  # Extract function
  if parsed.function != null {
    .function = parsed.function
  }
  if .function == null && parsed.func != null {
    .function = parsed.func
  }
  if .function == null && parsed.funcName != null {
    .function = parsed.funcName
  }

  # Extract line number
  if parsed.line != null {
    .line = parsed.line
  }
  if .line == null && parsed.lineno != null {
    .line = parsed.lineno
  }
  if .line == null && parsed.lineNumber != null {
    .line = parsed.lineNumber
  }

  # Extract file
  if parsed.file != null {
    .file = parsed.file
  }
  if .file == null && parsed.filename != null {
    .file = parsed.filename
  }
  if .file == null && parsed.pathname != null {
    .file = parsed.pathname
  }

  # Extract thread
  if parsed.thread != null {
    .thread = parsed.thread
  }
  if .thread == null && parsed.threadName != null {
    .thread = parsed.threadName
  }

  # Extract request_id
  if parsed.request_id != null {
    .request_id = parsed.request_id
  }
  if .request_id == null && parsed.requestId != null {
    .request_id = parsed.requestId
  }
  if .request_id == null && parsed.correlation_id != null {
    .request_id = parsed.correlation_id
  }
  if .request_id == null && parsed.trace_id != null {
    .request_id = parsed.trace_id
  }

  # Extract user
  if parsed.user != null {
    .user = parsed.user
  }
  if .user == null && parsed.user_id != null {
    .user = parsed.user_id
  }
  if .user == null && parsed.userId != null {
    .user = parsed.userId
  }

  # Extract duration
  if parsed.duration != null {
    .duration_ms = parsed.duration
  }
  if .duration_ms == null && parsed.duration_ms != null {
    .duration_ms = parsed.duration_ms
  }
  if .duration_ms == null && parsed.elapsed != null {
    .duration_ms = parsed.elapsed
  }
  if .duration_ms == null && parsed.latency != null {
    .duration_ms = parsed.latency
  }

  # Extract status code
  if parsed.status != null {
    .status_code = parsed.status
  }
  if .status_code == null && parsed.status_code != null {
    .status_code = parsed.status_code
  }
  if .status_code == null && parsed.statusCode != null {
    .status_code = parsed.statusCode
  }
  if .status_code == null && parsed.http_status != null {
    .status_code = parsed.http_status
  }

  # Extract method
  if parsed.method != null {
    .method = parsed.method
  }
  if .method == null && parsed.http_method != null {
    .method = parsed.http_method
  }

  # Extract path
  if parsed.path != null {
    .path = parsed.path
  }
  if .path == null && parsed.url != null {
    .path = parsed.url
  }
  if .path == null && parsed.endpoint != null {
    .path = parsed.endpoint
  }

  # Python structlog specific
  if parsed.event != null {
    .parsed_message = parsed.event
  }

  # Keep original for reference
  if .parsed_message != null {
    .original_message = msg
    .message = string!(.parsed_message)
  }
}

# ========================================
# FORMAT 2: Python standard logging
# ========================================
if .log_format == "unknown" {
  # Pattern: TIMESTAMP - LEVEL - LOGGER - MESSAGE
  pattern1 = parse_regex(msg, r'^(?P<ts>\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}[,\.]\d{3})\s*[-\s]\s*(?P<level>DEBUG|INFO|WARNING|WARN|ERROR|CRITICAL|FATAL)\s*[-\s]\s*(?P<logger>[\w\.]+)\s*[-\s]\s*(?P<message>.*)$') ?? null
  if pattern1 != null {
    .log_format = "python"
    .level = pattern1.level
    .logger = pattern1.logger
    .parsed_message = pattern1.message
    .log_timestamp = pattern1.ts
  }

  # Pattern: LEVEL:LOGGER:MESSAGE (Python basicConfig)
  if .log_format == "unknown" {
    pattern2 = parse_regex(msg, r'^(?P<level>DEBUG|INFO|WARNING|WARN|ERROR|CRITICAL|FATAL):(?P<logger>[\w\.]+):(?P<message>.*)$') ?? null
    if pattern2 != null {
      .log_format = "python"
      .level = pattern2.level
      .logger = pattern2.logger
      .parsed_message = pattern2.message
    }
  }

  # Pattern: [TIMESTAMP] LEVEL in MODULE: MESSAGE (Flask)
  if .log_format == "unknown" {
    pattern3 = parse_regex(msg, r'^\[(?P<ts>[^\]]+)\]\s+(?P<level>DEBUG|INFO|WARNING|WARN|ERROR|CRITICAL|FATAL)\s+in\s+(?P<logger>\w+):\s*(?P<message>.*)$') ?? null
    if pattern3 != null {
      .log_format = "python_flask"
      .level = pattern3.level
      .logger = pattern3.logger
      .parsed_message = pattern3.message
      .log_timestamp = pattern3.ts
    }
  }

  # Pattern: Django - TIMESTAMP LEVEL MESSAGE
  if .log_format == "unknown" {
    pattern4 = parse_regex(msg, r'^(?P<ts>\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})\s+(?P<level>DEBUG|INFO|WARNING|WARN|ERROR|CRITICAL|FATAL)\s+(?P<message>.*)$') ?? null
    if pattern4 != null {
      .log_format = "python_django"
      .level = pattern4.level
      .parsed_message = pattern4.message
      .log_timestamp = pattern4.ts
    }
  }
}

# ========================================
# FORMAT 3: Python Traceback detection
# ========================================
if .log_format == "unknown" || .log_format == "python" {
  if match(msg, r'Traceback \(most recent call last\):|^\s+File ".*", line \d+|^\s+raise |^[A-Z][a-zA-Z]*Error:|^[A-Z][a-zA-Z]*Exception:') {
    .log_format = "python_traceback"
    .level = "error"
    .is_traceback = true

    exc_match = parse_regex(msg, r'(?P<exc_type>[A-Z][a-zA-Z]*(?:Error|Exception)):\s*(?P<exc_message>.*)') ?? null
    if exc_match != null {
      .error_type = exc_match.exc_type
      .error_message = exc_match.exc_message
    }
  }
}

# ========================================
# FORMAT 4: Logfmt (key=value pairs)
# ========================================
if .log_format == "unknown" {
  if match(msg, r'^(\w+=[^\s]+\s*)+$') || match(msg, r'\blevel=\w+\b.*\bmsg=') {
    parsed_logfmt, err = parse_logfmt(msg)
    if err == null {
      .log_format = "logfmt"

      if parsed_logfmt.level != null {
        .level = parsed_logfmt.level
      }
      if .level == null && parsed_logfmt.lvl != null {
        .level = parsed_logfmt.lvl
      }

      if parsed_logfmt.msg != null {
        .parsed_message = parsed_logfmt.msg
      }
      if .parsed_message == null && parsed_logfmt.message != null {
        .parsed_message = parsed_logfmt.message
      }

      if parsed_logfmt.logger != null {
        .logger = parsed_logfmt.logger
      }
      if .logger == null && parsed_logfmt.component != null {
        .logger = parsed_logfmt.component
      }

      if parsed_logfmt.error != null {
        .error_type = parsed_logfmt.error
      }
      if .error_type == null && parsed_logfmt.err != null {
        .error_type = parsed_logfmt.err
      }

      if parsed_logfmt.duration != null {
        .duration_ms = parsed_logfmt.duration
      }
      if .duration_ms == null && parsed_logfmt.duration_ms != null {
        .duration_ms = parsed_logfmt.duration_ms
      }

      if parsed_logfmt.request_id != null {
        .request_id = parsed_logfmt.request_id
      }
      if .request_id == null && parsed_logfmt.trace_id != null {
        .request_id = parsed_logfmt.trace_id
      }
    }
  }
}

# ========================================
# FORMAT 5: Kubernetes klog format (Go)
# ========================================
if .log_format == "unknown" {
  klog_match = parse_regex(msg, r'^(?P<level>[IWEF])(?P<date>\d{4})\s+(?P<time>\d{2}:\d{2}:\d{2}\.\d+)\s+(?P<thread>\d+)\s+(?P<file>[^:]+):(?P<line>\d+)\]\s*(?P<message>.*)$') ?? null
  if klog_match != null {
    .log_format = "klog"
    .level = klog_match.level
    .file = klog_match.file
    .line = klog_match.line
    .thread = klog_match.thread
    .parsed_message = klog_match.message

    # Map klog levels
    if .level == "I" {
      .level = "info"
    }
    if .level == "W" {
      .level = "warn"
    }
    if .level == "E" {
      .level = "error"
    }
    if .level == "F" {
      .level = "fatal"
    }
  }
}

# ========================================
# FORMAT 6: Go zerolog/zap text format
# ========================================
if .log_format == "unknown" {
  go_match = parse_regex(msg, r'^(?:(?P<ts>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?|\d{2}:\d{2}:\d{2})\s+)?(?P<level>DBG|INF|WRN|ERR|FTL|TRC|DEBUG|INFO|WARN|WARNING|ERROR|FATAL|TRACE)\s+(?P<message>.*)$') ?? null
  if go_match != null {
    .log_format = "go"
    .level = go_match.level
    .parsed_message = go_match.message
    .log_timestamp = go_match.ts

    # Map short levels
    if .level == "DBG" || .level == "TRC" {
      .level = "debug"
    }
    if .level == "INF" {
      .level = "info"
    }
    if .level == "WRN" {
      .level = "warn"
    }
    if .level == "ERR" {
      .level = "error"
    }
    if .level == "FTL" {
      .level = "fatal"
    }
  }
}

# ========================================
# FORMAT 7: Java/Log4j format
# ========================================
if .log_format == "unknown" {
  java_match = parse_regex(msg, r'^(?:(?P<ts>\d{4}-\d{2}-\d{2}\s+)?\d{2}:\d{2}:\d{2}[,\.]\d{3})\s+(?P<level>TRACE|DEBUG|INFO|WARN|ERROR|FATAL)\s+\[(?P<thread>[^\]]+)\]\s+(?P<logger>[\w\.]+)\s*[-:]\s*(?P<message>.*)$') ?? null
  if java_match != null {
    .log_format = "java"
    .level = java_match.level
    .logger = java_match.logger
    .thread = java_match.thread
    .parsed_message = java_match.message
  }
}

# ========================================
# FORMAT 8: Syslog format
# ========================================
if .log_format == "unknown" {
  syslog_parsed = parse_syslog(msg) ?? null
  if syslog_parsed != null {
    .log_format = "syslog"
    .level = syslog_parsed.severity
    .parsed_message = syslog_parsed.message
    .syslog_facility = syslog_parsed.facility
    .syslog_hostname = syslog_parsed.hostname
    .syslog_appname = syslog_parsed.appname
  }
}

# ========================================
# FORMAT 9: Nginx/Apache access logs
# ========================================
if .log_format == "unknown" {
  nginx_match = parse_regex(msg, r'^(?P<ip>[\d\.]+)\s+\S+\s+\S+\s+\[(?P<ts>[^\]]+)\]\s+"(?P<method>\w+)\s+(?P<path>[^\s]+)\s+[^"]+"\s+(?P<status>\d+)\s+(?P<bytes>\d+)') ?? null
  if nginx_match != null {
    .log_format = "nginx_access"
    .client_ip = nginx_match.ip
    .method = nginx_match.method
    .path = nginx_match.path
    .status_code = to_int(nginx_match.status) ?? null
    .bytes = to_int(nginx_match.bytes) ?? null

    status = to_int!(nginx_match.status)
    if status >= 500 {
      .level = "error"
    }
    if status >= 400 && status < 500 {
      .level = "warn"
    }
    if status < 400 {
      .level = "info"
    }
  }
}

# ========================================
# FORMAT 10: Plain text with level detection
# ========================================
if .log_format == "unknown" {
  .log_format = "plain_text"

  if match(msg, r'(?i)\b(fatal|panic|critical)\b') {
    .level = "fatal"
  }
  if .level == null && match(msg, r'(?i)(\[error\]|level[=:]"?error|^error[:\s]|\berror\b.*\b(failed|exception|crash)|exception|traceback)') {
    .level = "error"
  }
  if .level == null && match(msg, r'(?i)(\[warn(ing)?\]|level[=:]"?warn|^warn(ing)?[:\s]|warning)') {
    .level = "warn"
  }
  if .level == null && match(msg, r'(?i)(\[debug\]|level[=:]"?debug|^debug[:\s])') {
    .level = "debug"
  }
  if .level == null && match(msg, r'(?i)(\[info\]|level[=:]"?info|^info[:\s])') {
    .level = "info"
  }
}

# ========================================
# NORMALIZE LEVEL
# ========================================
if .level != null {
  .level = downcase(string!(.level))
  if .level == "warning" {
    .level = "warn"
  }
  if .level == "critical" {
    .level = "fatal"
  }
  if .level == "trace" {
    .level = "debug"
  }
}

# Update message if we parsed a better one
if .parsed_message != null && .parsed_message != "" {
  .message = string!(.parsed_message)
}
