Tutorial

로그에서 타임스탬프 구문 분석: 실용 튜토리얼

소개

로그 파일에서 타임스탬프를 구문 분석하는 것은 DevOps, 시스템 관리자 및 개발자에게 중요한 기술입니다. 로그는 수많은 형식으로 제공되며 각각 타임스탬프 규칙이 다릅니다. 이 튜토리얼에서는 정규식 패턴과 검증된 구문 분석 전략을 사용하여 일반 로그 형식에서 타임스탬프를 안정적으로 추출하고 구문 분석하는 방법을 배웁니다.

일반적인 로그 형식

1. Apache 액세스 로그

형식:

127.0.0.1 - - [10/Jan/2024:15:30:45 +0000] "GET /api/users HTTP/1.1" 200 1234

타임스탬프 패턴: [DD/Mon/YYYY:HH:MM:SS +ZZZZ]

정규식 패턴

const apacheLogRegex = /\[(\d{2})\/(\w{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2}) ([+-]\d{4})\]/;

function parseApacheTimestamp(logLine) {
  const match = logLine.match(apacheLogRegex);
  if (!match) return null;

  const [, day, month, year, hour, minute, second, timezone] = match;

  // Month conversion
  const months = {
    'Jan': '01', 'Feb': '02', 'Mar': '03', 'Apr': '04',
    'May': '05', 'Jun': '06', 'Jul': '07', 'Aug': '08',
    'Sep': '09', 'Oct': '10', 'Nov': '11', 'Dec': '12'
  };

  // Build ISO 8601 timestamp
  const isoString = `${year}-${months[month]}-${day}T${hour}:${minute}:${second}${timezone.slice(0,3)}:${timezone.slice(3)}`;

  return {
    original: match[0],
    parsed: new Date(isoString),
    iso: isoString
  };
}

// Usage
const log = '127.0.0.1 - - [10/Jan/2024:15:30:45 +0000] "GET /api/users HTTP/1.1" 200 1234';
const result = parseApacheTimestamp(log);
console.log(result);
// {
//   original: '[10/Jan/2024:15:30:45 +0000]',
//   parsed: Date,
//   iso: '2024-01-10T15:30:45+00:00'
// }

파이썬 구현

import re
from datetime import datetime

apache_pattern = r'\[(\d{2})/(\w{3})/(\d{4}):(\d{2}):(\d{2}):(\d{2}) ([+-]\d{4})\]'

def parse_apache_timestamp(log_line):
    match = re.search(apache_pattern, log_line)
    if not match:
        return None

    day, month, year, hour, minute, second, tz = match.groups()

    # Parse timestamp
    timestamp_str = f"{day}/{month}/{year}:{hour}:{minute}:{second} {tz}"
    dt = datetime.strptime(timestamp_str, "%d/%b/%Y:%H:%M:%S %z")

    return {
        'original': match.group(0),
        'datetime': dt,
        'iso': dt.isoformat()
    }

# Usage
log = '127.0.0.1 - - [10/Jan/2024:15:30:45 +0000] "GET /api/users HTTP/1.1" 200 1234'
result = parse_apache_timestamp(log)
print(result)

2. Nginx 액세스 로그

형식:

192.168.1.1 - - [10/Jan/2024:15:30:45 +0000] "GET /api/data HTTP/1.1" 200 5678 "-" "Mozilla/5.0"

참고: 기본 Nginx 형식은 Apache 공통 로그 형식과 동일합니다.

사용자 정의 Nginx 형식:

# nginx.conf
log_format custom '$remote_addr - $remote_user [$time_local] "$request" '
                  '$status $body_bytes_sent "$http_referer" "$http_user_agent"'
                  ' rt=$request_time uct=$upstream_connect_time';

3. Syslog 형식(RFC 3164)

형식:

Jan 10 15:30:45 hostname application[1234]: Error occurred

타임스탬프 패턴: Mon DD HH:MM:SS

참고: 연도나 시간대가 없습니다! 추론해야 합니다.

Syslog 구문 분석

import re
from datetime import datetime

syslog_pattern = r'(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\S+)\s+(.*?):\s+(.*)'

def parse_syslog_timestamp(log_line, year=None):
    """
    Parse syslog timestamp (RFC 3164).
    Year must be provided as syslog format doesn't include it.
    """
    match = re.search(syslog_pattern, log_line)
    if not match:
        return None

    month, day, hour, minute, second, hostname, process, message = match.groups()

    # Use current year if not provided
    if year is None:
        year = datetime.now().year

    # Parse without timezone (assume local)
    timestamp_str = f"{month} {day} {year} {hour}:{minute}:{second}"
    dt = datetime.strptime(timestamp_str, "%b %d %Y %H:%M:%S")

    return {
        'datetime': dt,
        'hostname': hostname,
        'process': process,
        'message': message
    }

# Usage
log = 'Jan 10 15:30:45 web01 nginx[1234]: 404 error on /missing'
result = parse_syslog_timestamp(log, year=2024)

4. 애플리케이션 로그(ISO 8601)

공통 형식:

2024-01-10T15:30:45.123Z [INFO] Application started
2024-01-10T15:30:45.123+00:00 [ERROR] Connection failed
2024-01-10 15:30:45,123 INFO Starting process

범용 ISO 8601 파서

// Matches various ISO 8601 formats
const iso8601Patterns = [
  // With milliseconds and timezone
  /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2})/,
  /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/,
  // Without milliseconds
  /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2})/,
  /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)/,
  // Space-separated (common in logs)
  /(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/
];

function parseISO8601Timestamp(logLine) {
  for (const pattern of iso8601Patterns) {
    const match = logLine.match(pattern);
    if (match) {
      const timestamp = match[1];
      return {
        original: timestamp,
        parsed: new Date(timestamp.replace(' ', 'T')),
        format: 'ISO 8601'
      };
    }
  }
  return null;
}

// Usage
const logs = [
  '2024-01-10T15:30:45.123Z [INFO] Started',
  '2024-01-10 15:30:45 INFO: Process complete'
];

logs.forEach(log => {
  console.log(parseISO8601Timestamp(log));
});

5. Windows 이벤트 로그

형식:

01/10/2024 03:30:45 PM Information Application started

타임스탬프 패턴: MM/DD/YYYY HH:MM:SS AM/PM

import re
from datetime import datetime

windows_pattern = r'(\d{2}/\d{2}/\d{4})\s+(\d{1,2}:\d{2}:\d{2}\s+[AP]M)'

def parse_windows_timestamp(log_line):
    match = re.search(windows_pattern, log_line)
    if not match:
        return None

    date_str, time_str = match.groups()
    timestamp_str = f"{date_str} {time_str}"

    dt = datetime.strptime(timestamp_str, "%m/%d/%Y %I:%M:%S %p")

    return {
        'datetime': dt,
        'iso': dt.isoformat()
    }

고급 구문 분석 기술

1. 다중 형식 파서

단일 함수로 여러 로그 형식을 처리합니다.

import re
from datetime import datetime
from typing import Optional, Dict, Any

class LogTimestampParser:
    """Universal log timestamp parser supporting multiple formats."""

    def __init__(self):
        self.parsers = [
            ('apache', self._parse_apache),
            ('iso8601', self._parse_iso8601),
            ('syslog', self._parse_syslog),
            ('windows', self._parse_windows),
        ]

    def parse(self, log_line: str) -> Optional[Dict[str, Any]]:
        """Try all parsers until one succeeds."""
        for format_name, parser_func in self.parsers:
            try:
                result = parser_func(log_line)
                if result:
                    result['format'] = format_name
                    return result
            except Exception:
                continue
        return None

    def _parse_apache(self, line):
        pattern = r'\[(\d{2})/(\w{3})/(\d{4}):(\d{2}):(\d{2}):(\d{2}) ([+-]\d{4})\]'
        match = re.search(pattern, line)
        if match:
            timestamp_str = f"{match.group(1)}/{match.group(2)}/{match.group(3)}:{match.group(4)}:{match.group(5)}:{match.group(6)} {match.group(7)}"
            dt = datetime.strptime(timestamp_str, "%d/%b/%Y:%H:%M:%S %z")
            return {'datetime': dt, 'original': match.group(0)}
        return None

    def _parse_iso8601(self, line):
        # Multiple ISO patterns
        patterns = [
            (r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)', "%Y-%m-%dT%H:%M:%S.%fZ"),
            (r'(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)', "%Y-%m-%dT%H:%M:%SZ"),
            (r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', "%Y-%m-%d %H:%M:%S"),
        ]
        for pattern, fmt in patterns:
            match = re.search(pattern, line)
            if match:
                dt = datetime.strptime(match.group(1), fmt)
                return {'datetime': dt, 'original': match.group(1)}
        return None

    def _parse_syslog(self, line):
        pattern = r'(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})'
        match = re.search(pattern, line)
        if match:
            year = datetime.now().year
            timestamp_str = f"{match.group(1)} {match.group(2)} {year} {match.group(3)}:{match.group(4)}:{match.group(5)}"
            dt = datetime.strptime(timestamp_str, "%b %d %Y %H:%M:%S")
            return {'datetime': dt, 'original': match.group(0)}
        return None

    def _parse_windows(self, line):
        pattern = r'(\d{2}/\d{2}/\d{4})\s+(\d{1,2}:\d{2}:\d{2}\s+[AP]M)'
        match = re.search(pattern, line)
        if match:
            timestamp_str = f"{match.group(1)} {match.group(2)}"
            dt = datetime.strptime(timestamp_str, "%m/%d/%Y %I:%M:%S %p")
            return {'datetime': dt, 'original': f"{match.group(1)} {match.group(2)}"}
        return None

# Usage
parser = LogTimestampParser()

logs = [
    '127.0.0.1 - - [10/Jan/2024:15:30:45 +0000] "GET /"',
    '2024-01-10T15:30:45.123Z [INFO] Started',
    'Jan 10 15:30:45 server app: Error',
    '01/10/2024 03:30:45 PM Information'
]

for log in logs:
    result = parser.parse(log)
    if result:
        print(f"Format: {result['format']}, Time: {result['datetime']}")

2. 성능 최적화

대용량 로그 파일의 경우 성능이 중요합니다.

import re
from datetime import datetime
import mmap

class FastLogParser:
    """Optimized parser for large log files."""

    def __init__(self, timestamp_pattern, timestamp_format):
        self.pattern = re.compile(timestamp_pattern.encode())
        self.format = timestamp_format

    def parse_file(self, filepath):
        """Parse log file using memory mapping for speed."""
        timestamps = []

        with open(filepath, 'r+b') as f:
            # Memory-map the file
            with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mmapped:
                # Find all timestamp matches
                for match in self.pattern.finditer(mmapped):
                    timestamp_bytes = match.group(1)
                    timestamp_str = timestamp_bytes.decode('utf-8')

                    try:
                        dt = datetime.strptime(timestamp_str, self.format)
                        timestamps.append(dt)
                    except ValueError:
                        continue

        return timestamps

    def parse_file_streaming(self, filepath, batch_size=10000):
        """Stream parse large files in batches."""
        with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
            batch = []
            for line in f:
                match = re.search(self.pattern.pattern.decode(), line)
                if match:
                    try:
                        dt = datetime.strptime(match.group(1), self.format)
                        batch.append(dt)

                        if len(batch) >= batch_size:
                            yield batch
                            batch = []
                    except ValueError:
                        continue

            if batch:
                yield batch

# Usage - Apache logs
parser = FastLogParser(
    timestamp_pattern=rb'\[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2}) [+-]\d{4}\]',
    timestamp_format="%d/%b/%Y:%H:%M:%S"
)

# Parse entire file
timestamps = parser.parse_file('access.log')
print(f"Found {len(timestamps)} timestamps")

# Stream large file
for batch in parser.parse_file_streaming('huge.log'):
    print(f"Processing batch of {len(batch)} timestamps")
    # Process batch...

3. 시간대 처리

시간대 추출 및 정규화:

function extractTimezoneInfo(logLine) {
  // Common timezone patterns
  const patterns = [
    /([+-]\d{2}:?\d{2})$/,           // +00:00 or +0000
    /\s+([A-Z]{3,4})(?:\s|$)/,       // EST, EDT, UTC
    /\s+(Z)(?:\s|$)/                  // Z for UTC
  ];

  for (const pattern of patterns) {
    const match = logLine.match(pattern);
    if (match) {
      const tz = match[1];
      if (tz === 'Z') return 'UTC';
      if (/^[+-]\d/.test(tz)) return tz;
      return tz; // Named timezone
    }
  }

  return null; // No timezone found
}

// Convert all timestamps to UTC
function normalizeToUTC(timestamp, timezone) {
  const date = new Date(timestamp);

  if (timezone && timezone !== 'UTC') {
    // Handle timezone offset
    if (/^[+-]\d/.test(timezone)) {
      const offset = timezone.replace(':', '');
      const hours = parseInt(offset.slice(0, 3));
      const minutes = parseInt(offset.slice(0, 1) + offset.slice(3));

      date.setMinutes(date.getMinutes() - hours * 60 - minutes);
    }
  }

  return date;
}

실제 사용 사례

1. 로그 분석 파이프라인

from collections import defaultdict
from datetime import datetime
import re

class LogAnalyzer:
    """Analyze log files by parsing timestamps."""

    def __init__(self, parser):
        self.parser = parser
        self.stats = defaultdict(int)

    def analyze_file(self, filepath):
        """Analyze log file and generate statistics."""
        timestamps = []
        errors_by_hour = defaultdict(int)

        with open(filepath, 'r') as f:
            for line_num, line in enumerate(f, 1):
                # Parse timestamp
                result = self.parser.parse(line)
                if result:
                    dt = result['datetime']
                    timestamps.append(dt)

                    # Count errors by hour
                    if 'ERROR' in line or 'WARN' in line:
                        hour_key = dt.strftime('%Y-%m-%d %H:00')
                        errors_by_hour[hour_key] += 1
                else:
                    self.stats['unparsed_lines'] += 1

        # Generate statistics
        if timestamps:
            return {
                'total_lines': line_num,
                'parsed_timestamps': len(timestamps),
                'start_time': min(timestamps),
                'end_time': max(timestamps),
                'duration': max(timestamps) - min(timestamps),
                'errors_by_hour': dict(sorted(errors_by_hour.items())),
                'unparsed_lines': self.stats['unparsed_lines']
            }

        return None

# Usage
parser = LogTimestampParser()
analyzer = LogAnalyzer(parser)
stats = analyzer.analyze_file('application.log')

print(f"Log span: {stats['start_time']} to {stats['end_time']}")
print(f"Duration: {stats['duration']}")
print(f"Errors by hour: {stats['errors_by_hour']}")

모범 사례

1. 항상 구문 분석된 타임스탬프의 유효성을 검사하세요.

def is_valid_timestamp(dt, min_year=2000, max_year=2100):
    """Validate parsed timestamp is reasonable."""
    if not dt:
        return False

    if dt.year < min_year or dt.year > max_year:
        return False

    return True

2. 잘못된 로그를 적절하게 처리

def safe_parse(parser_func, line, default=None):
    """Safely parse with fallback."""
    try:
        result = parser_func(line)
        return result if result else default
    except Exception as e:
        logging.warning(f"Parse error: {e}")
        return default

3. 컴파일된 정규식 패턴 캐시

import re
from functools import lru_cache

@lru_cache(maxsize=128)
def get_compiled_pattern(pattern_str):
    """Cache compiled regex patterns."""
    return re.compile(pattern_str)

일반적인 함정

하지 마세요:

  • 모든 로그에 시간대가 있다고 가정합니다.
  • 버퍼링 없이 한 줄씩 구문 분석
  • 간단한 형식에는 값비싼 정규 표현식을 사용하세요.
  • 오류 처리 무시

해야 할 일:

  • 모든 타임스탬프를 UTC로 정규화
  • 대용량 파일에 메모리 매핑 사용
  • 정규식 패턴을 한 번 컴파일합니다.
  • 구문 분석된 결과의 유효성을 검사합니다.

관련 리소스