👨‍💻

syslog 負荷試験用アプリを Python で作成する

2023/11/06に公開

はじめに

syslog サーバの負荷試験用に大量の syslog を送信する必要があるのですが、ちょうどよいフリーのアプリがないため、ChatGPT を活用して syslog 負荷試験用のアプリを作りたいと思います。

事前準備

Windows クライアントのローカルで検証するため、事前に PowerShell で syslog サーバ相当を動作させ、ログ受信テストをします。

syslog サーバ相当 Powershell はこちらを参考にさせていただき、ChatGPT に指示して Close 処理を追加しました。(ありがとうございます)
https://level69.net/archives/24548

Run-Syslogserver.ps1
$Udp = New-Object Net.Sockets.UdpClient -ArgumentList 514
$Sender = $null

Add-Type -TypeDefinition @"
       public enum Syslog_Facility
       {
               kern,
               user,
               mail,
               system,
               security,
               syslog,
               lpr,
               news,
               uucp,
               clock,
               authpriv,
               ftp,
               ntp,
               logaudit,
               logalert,
               cron,
               local0,
               local1,
               local2,
               local3,
               local4,
               local5,
               local6,
               local7,
       }
"@
 
Add-Type -TypeDefinition @"
       public enum Syslog_Severity
       {
               Emergency,
               Alert,
               Critical,
               Error,
               Warning,
               Notice,
               Informational,
               Debug
       }
"@

try {
    while($true)
    {
        if($Udp.Available)
        {
            $Buffer = $Udp.Receive([ref]$Sender)
            $MessageString = [Text.Encoding]::UTF8.GetString($Buffer)
            $Priority = [Int]($MessageString -Replace "<|>.*")  
            [int]$FacilityInt = [Math]::truncate([decimal]($Priority / 8))
            $Facility = [Enum]::ToObject([Syslog_Facility], $FacilityInt)
            [int]$SeverityInt = $Priority - ($FacilityInt * 8 )
            $Severity = [Enum]::ToObject([Syslog_Severity], $SeverityInt)
            $MessageString = "$MessageString $Facility $Severity"
            $MessageString = $MessageString -Replace "<.*>",""
            #Write-Host $MessageString
            $MessageString >> c:\tmp\syslog.log
        }
        [Threading.Thread]::Sleep(500)
    }
}
finally {
    $Udp.Close()
}

こちらを実行して起動し、以下コマンドで syslog を送信します。

$syslogServer = "127.0.0.1"
$syslogPort = 514
$udpClient = New-Object System.Net.Sockets.UdpClient
$udpClient.Connect($syslogServer, $syslogPort)
$bytes = [Text.Encoding]::ASCII.GetBytes("<134>Oct 29 23:59:59 MyHost MyApp: Test message")
$udpClient.Send($bytes, $bytes.Length)
$udpClient.Close()

以下のように記録されていることを確認できました。

ChatGPT でアプリ作成

さまざま追加で指示しましたが、要件は以下の通りです。

  • 大量に syslog を送信する Web アプリを Python で作成する
  • Web UI で以下を指定する
    • ログ送信先
    • TCP or UDP
    • ポート番号
    • 1 秒間当たりのログ送信件数
    • 合計ログ送信件数
    • Facility
    • Severity
    • 送信するメッセージ
    • 送信メッセージに $msgId を使用することで一意の ID を埋め込む
    • メッセージ ID のプレフィックスを指定
  • ログの送信状況を確認できる
  • ログ送信を中断できる

一旦の完成形は以下になります。

app.py
from flask import Flask, render_template, request, jsonify

import socket
import threading
import time

app = Flask(__name__)

# グローバル変数
send_count = 0
cancel_flag = False


@app.route('/')
def index():
    return render_template('index.html')

@app.route('/send_logs', methods=['POST'])
def send_logs():
    global send_count
    global cancel_flag
    send_count = 0
    cancel_flag = False
    
    # フォームからのデータを取得
    log_destination = request.form.get('log_destination')
    protocol = request.form.get('protocol')
    port = int(request.form.get('port')) 
    logs_per_second = int(request.form.get('logs_per_second'))
    total_logs = int(request.form.get('total_logs'))
    facility = int(request.form.get('facility'))
    severity = int(request.form.get('severity'))
    message = request.form.get('message')
    msg_id_prefix = request.form.get('msg_id_prefix')

    # Priorityの計算
    priority = (facility * 8) + severity

    # ログの送信を別スレッドで実行
    def worker():
        global send_count
        sock_type = socket.SOCK_DGRAM if protocol == 'UDP' else socket.SOCK_STREAM
        with socket.socket(socket.AF_INET, sock_type) as s:
            if protocol == 'TCP':
                s.connect((log_destination, port))
            for i in range(total_logs):
                if send_count >= total_logs or cancel_flag:
                    break
                unique_id = f"{msg_id_prefix}{i}"
                msg = f"<{priority}>{message.replace('$msgId', unique_id)}"
                if protocol == 'UDP':
                    s.sendto(msg.encode(), (log_destination, port))
                else:
                    s.send(msg.encode())
                send_count += 1
                time.sleep(1.0 / logs_per_second)
                
    thread = threading.Thread(target=worker)
    thread.start()

    return jsonify(success=True)


@app.route('/cancel_send', methods=['POST'])
def cancel_send():
    global cancel_flag
    cancel_flag = True
    return jsonify(success=True)


@app.route('/status', methods=['GET'])
def status():
    return jsonify(count=send_count)

if __name__ == "__main__":
    app.run(debug=True)
index.html
<!DOCTYPE html>
<html>
<head>
    <title>Syslog Load Tester</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
    <h2>Syslog Load Tester</h2>
    <form id="logForm">
        Log Destination: <input type="text" name="log_destination"><br><br>
        Protocol: 
        <select name="protocol">
            <option value="UDP">UDP</option>
            <option value="TCP">TCP</option>
        </select><br><br>
        Port: <input type="text" name="port" value="514"><br><br>
        Logs per second: <input type="number" name="logs_per_second" value="1"><br><br>
        Total logs: <input type="number" name="total_logs" value="10"><br><br>
       
        Facility:
        <select name="facility">
            <option value="0">kern</option>
            <option value="1">user</option>
            <option value="2">mail</option>
            <option value="3">system</option>
            <option value="4">security</option>
            <option value="5">syslog</option>
            <option value="6">lpr</option>
            <option value="7">news</option>
            <option value="8">uucp</option>
            <option value="9">clock</option>
            <option value="10">authpriv</option>
            <option value="11">ftp</option>
            <option value="12">ntp</option>
            <option value="13">logaudit</option>
            <option value="14">logalert</option>
            <option value="15">cron</option>
            <option value="16">local0</option>
            <option value="17">local1</option>
            <option value="18">local2</option>
            <option value="19">local3</option>
            <option value="20">local4</option>
            <option value="21">local5</option>
            <option value="22">local6</option>
            <option value="23">local7</option>
        </select><br><br>

        Severity:
        <select name="severity">
            <option value="0">Emergency</option>
            <option value="1">Alert</option>
            <option value="2">Critical</option>
            <option value="3">Error</option>
            <option value="4">Warning</option>
            <option value="5">Notice</option>
            <option value="6">Informational</option>
            <option value="7">Debug</option>
        </select><br><br>

        Message: <input type="text" name="message" value="Test log $msgId"><br><br>
        Message ID Prefix: <input type="text" name="msg_id_prefix" value="log-"><br><br>
        
        <button type="button" onclick="sendLogs()">Send Logs</button>
        <button type="button" onclick="cancelSend()">Cancel</button><br><br>
    </form>

    <div id="status">
        Logs sent: 0
    </div>

    <script>
        function sendLogs() {
            $.post("/send_logs", $("#logForm").serialize()).done(function() {
                updateStatus();
            });
        }

        function updateStatus() {
            $.get("/status", function(data) {
                $("#status").text("Logs sent: " + data.count);
                if (data.count < parseInt($("input[name='total_logs']").val())) {
                    setTimeout(updateStatus, 1000);
                }
            });
        }
    </script>
    <script>
        function cancelSend() {
            fetch('/cancel_send', {method: 'POST'})
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    alert('Log sending canceled.');
                }
            });
        }
    </script>
</body>
</html>

実行イメージは以下の通りです。

課題など

実際に負荷試験用として使用してみると、time.sleep の影響でぴったりと件数/秒が実現できず、また 1,000 件/秒あたりが限界かと思います。time.sleep の箇所をコメントアウトすれば、秒間の送信件数は制御できないですが、大量の送信 (10,000 件以上) の動作はしました。このあたりが利用するにあたっての留意点や課題かと思います。

Microsoft (有志)

Discussion