Flaskを用いたログダッシュボードの作成と監視データの可視化

本記事では、これまで作成した死活監視スクリプトで取得したログを活用し、過去の監視データを集計・可視化する方法をご紹介します。Flaskを使用したログデータの保存・集計システムと、そのデータを可視化するダッシュボードの構築手順を解説します。

以下の画像は完成したダッシュボードの例です:


使用技術

以下の技術を使用して実装しました:

  • Python / Flask: WebアプリケーションとAPIの構築。
  • HTML/CSS/JavaScript: ダッシュボードのフロントエンド構築。
  • CSV: ログデータの保存形式。
  • CGI: FlaskアプリをCGIスクリプトとして実行。

ファイル構成

プロジェクトのディレクトリ構造は以下の通りです:

/site_monitor/
│
├── logs/
│   ├── analyzegear/
│   │   ├── site1/
│   │   │   ├── monitor_results_202411.csv
│   │   │   ├── monitor_results_202410.csv
│   │   │   ...
│   │   ├── site2/
│   │       ├── monitor_results_202411.csv
│   │       ├── monitor_results_202410.csv
│   ...
├── app/
│   ├── main.py        # Flaskアプリケーション
│   ├── templates/
│   │   └── index.html # ダッシュボードHTML
│   ├── static/
│       ├── styles.css # カスタムCSS
│       ├── script.js  # カスタムJavaScript
│
├── .htaccess          # Apache設定ファイル
├── index.cgi          # FlaskアプリをCGI経由で実行

コード一覧

1. .htaccess

Options +ExecCGI
AddHandler cgi-script .cgi
DirectoryIndex index.cgi

# すべてのリクエストを index.cgi にリダイレクト
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /index.cgi/$1 [QSA,L]

2. index.cgi

#!/home/youruser/miniconda3/envs/flask_env/bin/python

from main import app
import os

# CGI ハンドラー
def cgi_handler():

    from wsgiref.handlers import CGIHandler

    # Flask アプリケーションをCGI経由で実行
    CGIHandler().run(app)

if __name__ == '__main__':
    # デバッグ用コードを削除
    cgi_handler()

3. Flaskアプリ (main.py)

from flask import Flask, jsonify, render_template
import os
import csv
from collections import defaultdict
from datetime import datetime

app = Flask(__name__)

# ログ保存ベースディレクトリ
LOG_DIR = "./logs"

def parse_logs(company=None, site=None, year_month=None):
    """
    ログを会社、サイト、年月で絞り込んで集計する
    """
    data = defaultdict(lambda: {"total": 0, "success": 0, "failure": 0, "status_codes": defaultdict(int)})
    companies = [company] if company else os.listdir(LOG_DIR)

    for comp in companies:
        company_dir = os.path.join(LOG_DIR, comp)
        if not os.path.isdir(company_dir):
            continue

        site_dirs = [site] if site else os.listdir(company_dir)
        for site_dir in site_dirs:
            site_path = os.path.join(company_dir, site_dir)
            if not os.path.isdir(site_path):
                continue

            for log_file in os.listdir(site_path):
                if log_file.endswith(".csv"):
                    file_date = log_file.split("_")[-1].replace(".csv", "")
                    if year_month and not file_date.startswith(year_month):
                        continue

                    with open(os.path.join(site_path, log_file), mode="r") as f:
                        reader = csv.DictReader(f)
                        for row in reader:
                            site_url = row["サイト"]
                            status = row["状態"]
                            status_code = row["ステータスコード"]

                            data[site_url]["total"] += 1
                            if status == "正常に稼働中":
                                data[site_url]["success"] += 1
                            else:
                                data[site_url]["failure"] += 1
                            data[site_url]["status_codes"][status_code] += 1

    return {"logs": data}

def get_site_list(company):
    """
    会社内のすべてのサイトを取得
    """
    sites = []
    company_dir = os.path.join(LOG_DIR, company)
    if not os.path.isdir(company_dir):
        return sites

    for site_dir in os.listdir(company_dir):
        site_path = os.path.join(company_dir, site_dir)
        if os.path.isdir(site_path):
            sites.append(site_dir)

    return sites

def get_year_month_list():
    """
    ログディレクトリから年月を動的に取得
    """
    year_months = set()
    for company_dir in os.listdir(LOG_DIR):
        company_path = os.path.join(LOG_DIR, company_dir)
        if not os.path.isdir(company_path):
            continue

        for site_dir in os.listdir(company_path):
            site_path = os.path.join(company_path, site_dir)
            if not os.path.isdir(site_path):
                continue

            for log_file in os.listdir(site_path):
                if log_file.endswith(".csv"):
                    file_date = log_file.split("_")[-1].replace(".csv", "")
                    year_months.add(file_date[:6])

    return sorted(year_months)

@app.route("/")
def index():
    year_months = get_year_month_list()
    all_data = parse_logs()
    companies = os.listdir(LOG_DIR)
    return render_template("index.html", companies=companies, initial_logs=all_data["logs"], year_months=year_months)

@app.route("/api/all")
def api_all():
    logs = parse_logs()
    return jsonify(logs)

@app.route("/api/<company>")
def api_company(company):
    logs = parse_logs(company)
    sites = get_site_list(company)
    return jsonify({"logs": logs["logs"], "sites": sites})

@app.route("/api/<company>/<site>")
def api_company_site(company, site):
    logs = parse_logs(company, site)
    return jsonify(logs)

@app.route("/api/<company>/<site>/<year_month>")
def api_company_site_year_month(company, site, year_month):
    logs = parse_logs(company, site, year_month)
    return jsonify(logs)

@app.route("/api/<company>/<year_month>")
def api_company_year_month(company, year_month):
    logs = parse_logs(company=company, year_month=year_month)
    return jsonify(logs)

if __name__ == "__main__":
    app.run(debug=True)

4. HTML (index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログダッシュボード</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <h1>ログダッシュボード</h1>
    <form id="selection-form">
        <label for="company">会社を選択:</label>
        <select id="company">
            <option value="all" selected>全体を表示</option>
            {% for company in companies %}
            <option value="{{ company }}">{{ company }}</option>
            {% endfor %}
        </select>

        <label for="site">サイトを選択:</label>
        <select id="site" disabled>
            <option value="all" selected>全体を表示</option>
        </select>

        <label for="year_month">年月を選択:</label>
        <select id="year_month">
            <option value="all" selected>全体を表示</option>
            {% for year_month in year_months %}
            <option value="{{ year_month }}">{{ year_month }}</option>
            {% endfor %}
        </select>
    </form>

    <div id="logs-container"></div>

    <script src="/static/script.js"></script>
</body>
</html>

5. CSS (styles.css)

body {
    font-family: Arial, sans-serif;
    margin: 20px;
}
table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 20px;
}
th, td {
    border: 1px solid #ddd;
    padding: 8px;
    text-align: center;
}
th {
    background-color: #f4f4f4;
}
select, button {
    margin: 5px;
}
.bar-chart {
    height: 20px;
    display: flex;
}
.bar-success {
    background-color: #4caf50;
}
.bar-failure {
    background-color: #f44336;
}

6. JavaScript (script.js)

const companySelect = document.getElementById("company");
const siteSelect = document.getElementById("site");
const yearMonthSelect = document.getElementById("year_month");
const logsContainer = document.getElementById("logs-container");

async function fetchData(url) {
    const response = await fetch(url);
    if (!response.ok) {
        console.error("データの取得に失敗しました:", response.status);
        return null;
    }
    return await response.json();
}

async function initialize() {
    const allData = await fetchData("/api/all");
    if (allData) {
        renderLogs(allData.logs);
    }
}

function renderLogs(logs) {
    logsContainer.innerHTML = `
        <table>
            <thead>
                <tr>
                    <th>サイト</th>
                    <th>合計リクエスト数</th>
                    <th>正常</th>
                    <th>異常</th>
                    <th>稼働率</th>
                </tr>
            </thead>
            <tbody>
                ${Object.entries(logs).map(([site, data]) => {
                    const successRate = (data.success / data.total) * 100 || 0;
                    return `
                        <tr>
                            <td>${site}</td>
                            <td>${data.total}</td>
                            <td>${data.success}</td>
                            <td>${data.failure}</td>
                            <td>
                                <div class="bar-chart">
                                    <div class="bar-success" style="width: ${successRate}%"></div>
                                    <div class="bar-failure" style="width: ${100 - successRate}%"></div>
                                </div>
                            </td>
                        </tr>`;
                }).join("")}
            </tbody>
        </table>
    `;
}

companySelect.addEventListener("change", async () => {
    const company = companySelect.value;
    if (company === "all") {
        siteSelect.disabled = true;
        yearMonthSelect.disabled = false;
        initialize();
        return;
    }
    const data = await fetchData(`/api/${company}`);
    if (data) {
        renderLogs(data.logs);
        siteSelect.innerHTML = '<option value="all" selected>全体を表示</option>';
        data.sites.forEach(site => {
            siteSelect.innerHTML += `<option value="${site}">${site}</option>`;
        });
        siteSelect.disabled = false;
    }
});

siteSelect.addEventListener("change", async () => {
    const company = companySelect.value;
    const site = siteSelect.value;
    const yearMonth = yearMonthSelect.value;
    const url = site === "all"
        ? `/api/${company}/${yearMonth}`
        : `/api/${company}/${site}/${yearMonth}`;
    const data = await fetchData(url);
    if (data) renderLogs(data.logs);
});

yearMonthSelect.addEventListener("change", async () => {
    const company = companySelect.value;
    const site = siteSelect.value;
    const yearMonth = yearMonthSelect.value;
    const url = site === "all"
        ? `/api/${company}/${yearMonth}`
        : `/api/${company}/${site}/${yearMonth}`;
    const data = await fetchData(url);
    if (data) renderLogs(data.logs);
});

initialize();

APIレスポンス例

/api/all

{
    "https://example.com/": {
        "total": 100,
        "success": 98,
        "failure": 2,
        "status_codes": {
            "200": 98,
            "500": 2
        }
    },
    "https://analyzegear.co.jp/": {
        "total": 120,
        "success": 115,
        "failure": 5,
        "status_codes": {
            "200": 115,
            "404": 5
        }
    }
}

まとめ

このプロジェクトでは、監視データを効率的に集計・表示できるダッシュボードをFlaskと簡単なHTML/CSS/JavaScriptで構築しました。サーバーの稼働状況をリアルタイムで把握でき、異常検知の迅速な対応に役立ちます。監視システムの改良や、さらに詳細なグラフや通知機能を追加することで、運用の効率化を図ることが可能です。

コメント

タイトルとURLをコピーしました