Tutorial
Analisando carimbos de data/hora de logs: tutorial prático
Introdução
A análise de carimbos de data/hora de arquivos de log é uma habilidade crítica para DevOps, administradores de sistema e desenvolvedores. Os logs vêm em inúmeros formatos, cada um com diferentes convenções de carimbo de data/hora. Este tutorial ensina você a extrair e analisar carimbos de data/hora de formatos de log comuns usando padrões regex e estratégias de análise comprovadas.
Formatos de registro comuns
1. Logs de acesso do Apache
Formato:
127.0.0.1 - - [10/Jan/2024:15:30:45 +0000] "GET /api/users HTTP/1.1" 200 1234
Padrão de carimbo de data/hora: [DD/Mon/YYYY:HH:MM:SS +ZZZZ]
Padrão Regex
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'
// }
Implementação Python
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. Registros de acesso Nginx
Formato:
192.168.1.1 - - [10/Jan/2024:15:30:45 +0000] "GET /api/data HTTP/1.1" 200 5678 "-" "Mozilla/5.0"
Observação: O formato Nginx padrão é idêntico ao Apache Common Log Format.
Formato Nginx personalizado:
# 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. Formato Syslog (RFC 3164)
Formato:
Jan 10 15:30:45 hostname application[1234]: Error occurred
Padrão de carimbo de data/hora: Mon DD HH:MM:SS
Observação: Sem ano ou fuso horário! Deve ser inferido.
Analisando 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. Registros de aplicativos (ISO 8601)
Formatos comuns:
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
Analisador Universal 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. Logs de eventos do Windows
Formato:
01/10/2024 03:30:45 PM Information Application started
Padrão de carimbo de data/hora: 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()
}
Técnicas avançadas de análise
1. Analisador multiformato
Lide com vários formatos de log em uma única função:
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. Otimização de desempenho
Para arquivos de log grandes, o desempenho é importante:
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. Tratamento de fuso horário
Extraia e normalize fusos horários:
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;
}
Casos de uso prático
1. Pipeline de análise de log
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']}")
Melhores práticas
1. Sempre valide carimbos de data/hora analisados
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. Lidar com logs malformados com elegância
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. Padrões Regex compilados em cache
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)
Armadilhas Comuns
❌ Não:
- Suponha que todos os logs tenham fusos horários
- Analisar linha por linha sem buffer
- Use regex caro para formatos simples
- Ignorar tratamento de erros
✅ Faça:
- Normalizar todos os carimbos de data/hora para UTC
- Use mapeamento de memória para arquivos grandes
- Compilar padrões regex uma vez
- Validar resultados analisados
Recursos relacionados
- Práticas recomendadas para validação de carimbo de data/hora
- Tratamento de alterações de horário de verão
- Folha de dicas do formato Timestamp
- [Conversor de carimbo de data e hora em lote](/conversor de carimbo de data e hora em lote)