OPEN HYPER STEP
← 목록으로 (화이트햇 보안)
SECURITY · 77 / 84
security
CHAPTER 77 / 84
읽기 약 2
FUNCTION

로그 시각화와 리포트


핵심 개념

collections.Counter + matplotlib/plotly로 24시간 로그 분석 대시보드를 만든다.

본문

핵심 통계 — Counter 기반

PYTHON📋 코드 (53줄)
# ⚠️ 이 코드는 허가된 환경에서만 사용하세요.
from collections import Counter, defaultdict
from datetime import datetime
import re

APACHE_COMBINED = re.compile(
    r'^(?P<ip>\S+)\s+\S+\s+\S+\s+'
    r'\[(?P<time>[^\]]+)\]\s+'
    r'"(?P<method>\S+)\s+(?P<path>\S+)\s+\S+"\s+'
    r'(?P<status>\d{3})\s+(?P<bytes>\S+)\s+'
    r'"(?P<referer>[^"]*)"\s+"(?P<user_agent>[^"]*)"'
)


def analyze_log(log_path: str) -> dict:
    """단일 순회로 모든 통계 수집."""
    stats = {
        'total': 0,
        'ips': Counter(),
        'paths': Counter(),
        'methods': Counter(),
        'statuses': Counter(),
        'user_agents': Counter(),
        'hourly': Counter(),  # 시간대별 (00~23)
        'bytes_total': 0,
    }

    with open(log_path, encoding='utf-8', errors='replace') as f:
        for line in f:
            m = APACHE_COMBINED.match(line)
            if not m:
                continue
            stats['total'] += 1
            stats['ips'][m['ip']] += 1
            stats['paths'][m['path']] += 1
            stats['methods'][m['method']] += 1
            stats['statuses'][int(m['status'])] += 1
            stats['user_agents'][m['user_agent'][:80]] += 1

            # 시간대 추출
            try:
                t = datetime.strptime(m['time'].split()[0], '%d/%b/%Y:%H:%M:%S')
                stats['hourly'][t.hour] += 1
            except ValueError:
                pass

            # 바이트
            try:
                stats['bytes_total'] += int(m['bytes'])
            except ValueError:
                pass

    return stats

matplotlib 시각화

PYTHON📋 코드 (44줄)
# pip install matplotlib
import matplotlib.pyplot as plt

def plot_dashboard(stats: dict, output_path: str = 'log_dashboard.png'):
    """4개 차트 종합 대시보드."""
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    fig.suptitle('웹 로그 분석 대시보드', fontsize=16, fontweight='bold')

    # 1. 시간대별 요청 수
    hours = list(range(24))
    counts = [stats['hourly'].get(h, 0) for h in hours]
    axes[0, 0].bar(hours, counts, color='#6366f1')
    axes[0, 0].set_title('시간대별 요청 수')
    axes[0, 0].set_xlabel('시간 (24h)')
    axes[0, 0].set_ylabel('요청 수')
    axes[0, 0].set_xticks(hours[::2])

    # 2. 상태 코드 분포
    statuses = sorted(stats['statuses'].items())
    codes = [str(s[0]) for s in statuses]
    counts = [s[1] for s in statuses]
    colors = ['#10b981' if int(c) < 400 else '#f59e0b' if int(c) < 500 else '#ef4444' for c in codes]
    axes[0, 1].bar(codes, counts, color=colors)
    axes[0, 1].set_title('상태 코드 분포')
    axes[0, 1].set_xlabel('HTTP 상태')

    # 3. Top 10 IP
    top_ips = stats['ips'].most_common(10)
    ips = [x[0] for x in reversed(top_ips)]
    counts = [x[1] for x in reversed(top_ips)]
    axes[1, 0].barh(ips, counts, color='#a78bfa')
    axes[1, 0].set_title('Top 10 IP (요청 수)')

    # 4. Top 10 Path
    top_paths = stats['paths'].most_common(10)
    paths = [x[0][:30] for x in reversed(top_paths)]
    counts = [x[1] for x in reversed(top_paths)]
    axes[1, 1].barh(paths, counts, color='#34d399')
    axes[1, 1].set_title('Top 10 경로 (요청 수)')

    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    return output_path

Plotly — 인터랙티브 HTML 대시보드

PYTHON📋 코드 (51줄)
# pip install plotly
import plotly.graph_objects as go
from plotly.subplots import make_subplots

def plotly_dashboard(stats: dict, output_path: str = 'dashboard.html'):
    fig = make_subplots(
        rows=2, cols=2,
        subplot_titles=('시간대별 요청', '상태 코드', 'Top IP', 'Top Path'),
    )

    # 시간대별
    hours = list(range(24))
    fig.add_trace(go.Bar(
        x=hours,
        y=[stats['hourly'].get(h, 0) for h in hours],
        marker_color='#6366f1',
    ), row=1, col=1)

    # 상태 코드
    statuses = sorted(stats['statuses'].items())
    fig.add_trace(go.Bar(
        x=[str(s[0]) for s in statuses],
        y=[s[1] for s in statuses],
        marker_color=['#10b981' if s[0] < 400 else '#f59e0b' if s[0] < 500 else '#ef4444' for s in statuses],
    ), row=1, col=2)

    # Top IP
    top_ips = stats['ips'].most_common(10)
    fig.add_trace(go.Bar(
        x=[x[1] for x in top_ips],
        y=[x[0] for x in top_ips],
        orientation='h',
        marker_color='#a78bfa',
    ), row=2, col=1)

    # Top Path
    top_paths = stats['paths'].most_common(10)
    fig.add_trace(go.Bar(
        x=[x[1] for x in top_paths],
        y=[x[0][:30] for x in top_paths],
        orientation='h',
        marker_color='#34d399',
    ), row=2, col=2)

    fig.update_layout(
        height=800,
        title_text='웹 로그 분석 대시보드',
        showlegend=False,
    )
    fig.write_html(output_path)
    return output_path

CSV 리포트

PYTHON📋 코드 (17줄)
import csv

def export_csv(stats: dict, output_path: str = 'log_report.csv'):
    """주요 통계를 CSV로 저장 — Excel 분석용."""
    with open(output_path, 'w', newline='', encoding='utf-8-sig') as f:
        w = csv.writer(f)
        w.writerow(['지표', '값'])
        w.writerow(['총 요청 수', stats['total']])
        w.writerow(['총 트래픽 (MB)', f'{stats["bytes_total"] / 1024 / 1024:.2f}'])
        w.writerow([])
        w.writerow(['Top 10 IP'])
        for ip, count in stats['ips'].most_common(10):
            w.writerow([ip, count])
        w.writerow([])
        w.writerow(['상태 코드 분포'])
        for status, count in sorted(stats['statuses'].items()):
            w.writerow([status, count])

JSON 리포트

PYTHON📋 코드 (17줄)
import json

def export_json(stats: dict, output_path: str = 'log_report.json'):
    """기계 읽기 좋은 형식 — 다른 도구 연동용."""
    serializable = {
        'total': stats['total'],
        'bytes_total': stats['bytes_total'],
        'top_ips': stats['ips'].most_common(20),
        'top_paths': stats['paths'].most_common(20),
        'methods': dict(stats['methods']),
        'statuses': dict(stats['statuses']),
        'hourly': dict(stats['hourly']),
        'top_user_agents': stats['user_agents'].most_common(20),
        'generated_at': datetime.now().isoformat(),
    }
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(serializable, f, indent=2, ensure_ascii=False)

실습: 24시간 분석 자동화

PYTHON📋 코드 (18줄)
def daily_report(log_path: str, output_dir: str = 'reports'):
    """매일 실행되는 분석 → PNG + HTML + CSV + JSON 4종 출력."""
    import os
    os.makedirs(output_dir, exist_ok=True)
    today = datetime.now().strftime('%Y-%m-%d')

    print(f'📊 분석 시작: {log_path}')
    stats = analyze_log(log_path)

    plot_dashboard(stats, f'{output_dir}/dashboard_{today}.png')
    plotly_dashboard(stats, f'{output_dir}/dashboard_{today}.html')
    export_csv(stats, f'{output_dir}/report_{today}.csv')
    export_json(stats, f'{output_dir}/report_{today}.json')

    print(f'\n총 요청: {stats["total"]:,}')
    print(f'트래픽: {stats["bytes_total"] / 1024 / 1024:.1f} MB')
    print(f'Top IP: {stats["ips"].most_common(3)}')
    print(f'\n📁 저장: {output_dir}/')

⚠️ 시각화 흔한 실수

  • 너무 많은 카테고리 → 비교 어려움 (Top 10이 적정)
  • y축 0부터 안 시작 → 차이 과장
  • 색상이 의미 전달 안 함 → 빨강/노랑/초록으로 상태 매핑
  • PII(개인정보) 노출 — IP를 마스킹하거나 해시 처리

AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 로그 분석 코드의
Counter·시각화·메모리 사용을 분석하고
10GB+ 로그도 처리할 수 있게 개선해줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

matplotlib vs plotly vs Streamlit
로그 대시보드 만들 때 각각의 장단점을
실전 코드로 비교해줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 한 달치 웹 로그를 분석해서
시간대·요일·경로별 트래픽 패턴과
이상 징후를 종합 리포트로 만들어줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 로그 시각화 트렌드 —
자체 plotly vs Grafana vs Datadog Dashboard
1인 운영자에게 가성비 좋은 선택을 솔직히 알려줘.

⭐ 이것만 기억하세요
로그 시각화와 리포트 이 3가지만 확실히 잡으세요
1.Counter + matplotlib + plotly 조합으로 4개 차트 종합 대시보드를 한 함수로 만든다
2.4종 출력(PNG/HTML/CSV/JSON)을 자동화하면 매일 다른 사람과 데이터를 공유할 수 있다
3.다음 챕터에서 모듈 4 종합 — 로그 분석 자동화 도구 CLI를 완성한다


공유하기
진행도 77 / 84