生成AIでサーバーに耳を傾けてみた

■ はじめに

こんにちは。SREの平です。今回はたくさんのログからサーバーがもしもなにか危ない状態だったら通知してくれるような状態を作ってみました。

概要としては、自宅の余っているGPU(NVIDIA GeForce RTX 2060 6GB)を使い、ローカルLLMを立ち上げて、自宅サーバーの/var/log/syslogを投げて、サマリーと重要度を出してもらい、重要度がwarnであればdiscordに投げてもらって私がサーバーの異変に気づく!という仕組みです。

ただ生成AIなので、レスポンスが100%絶対指定した通り、にはならないことがあります。その場合parseエラーが出たり出なかったりと、常用にはもう少し寛大な(笑)処理になるようにしていかないといけないので、こんな方法もあるんだな、という提案レベルで見ていただければと思います。

■ Server

何はともあれ、最初にDiscordでログを投げたいサーバーのウェブフックを用意してください。 サーバーを右クリック>連携サービス>ウェブフック で確認や作成が出来ます。

次にサーバーの準備をしていきます。LLM実行ツールはOllamaにしました。

Ollama https://github.com/ollama/ollama

サーバーはUbuntuです。ドキュメントに従いインストールします。

curl -fsSL https://ollama.com/install.sh | sh
sudo reboot

再起動後、必要なモデルをpullします。 今回はGPUが6GBしかないのでそれに収まりつつ、希望に近いJSONを出してくれる回数が多かったGemma 2:9Bを選択しました。

ollama pull gemma2

次にdebug用ログファイルを3つ作成します。

  • /var/log/receive.log ←fluentから受け取ったログを出力する。
  • /var/log/sendllm.log ←LLMに送るプロンプトを出力する。
  • /var/log/response.log ←LLMからのレスポンスを出力する。

そして今回の中心となるWebAPIを作成します。 今回はテストなのでそのまま動かしてしまいましたが、サービス化するには適当なwebサーバーと組み合わせましょう。 これを起動させると5000番で待ち受けて、届いたJSONをプロンプトと一緒にlocalhostのOllamaに投げつける、という動きをしています。 文中のDISCORD_WEBHOOK_URLは各自書き換えてください。

この中で苦労したところはプロンプトで、思い通りのJSONで(できるだけ)返してもらうようにするところで、経験的に100発95中くらいにはなりました。

  • 出力方法を限定する ←これが一番効く
  • 指示をinstructionsタグ、ログはlog_dataタグで囲む
  • 「JSONだぞ!絶対にJSONだぞ!JSONで返せよ!」を繰り返す(笑)

app.py

from flask import Flask, request
import json
import requests
import threading

app = Flask(__name__)

LOG_FILE_PATH = '/var/log/receive.log'
SENDLLM_LOG_FILE_PATH = '/var/log/sendllm.log'
RESPONSE_LOG_FILE_PATH = '/var/log/response.log'
DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/your_webhook_url'

# ログ書き込み関数
def write_log(file_path, content):
    with open(file_path, 'a') as log_file:
        log_file.write(content + "\n")

# LLMへのリクエスト送信とレスポンス処理
def send_to_llm(llm_request_payload):
    try:
        response = requests.post("http://localhost:11434/api/chat", json=llm_request_payload)
        response_data = response.json()
        write_log(RESPONSE_LOG_FILE_PATH, json.dumps(response_data, indent=2))
        process_llm_response(response_data)
    except requests.exceptions.RequestException as e:
        write_log(RESPONSE_LOG_FILE_PATH, f"Error occurred while sending request to LLM: {e}")

# LLMのレスポンス処理
def process_llm_response(response_data):
    if "message" in response_data and "content" in response_data["message"]:
        response_content = json.loads(response_data["message"]["content"])
        if response_content.get("severity") == "warn":
            send_discord_notification(response_content)

# Discord通知送信
def send_discord_notification(response_content):
    message = f"**Warning Alert**\n\nSummary: {response_content['summary']}\nSeverity: {response_content['severity']}"
    try:
        requests.post(DISCORD_WEBHOOK_URL, json={"content": message})
    except requests.exceptions.RequestException as e:
        write_log(RESPONSE_LOG_FILE_PATH, f"Failed to send Discord notification: {e}")

@app.route('/receive_logs', methods=['POST'])
def receive_logs():
    data = request.get_json()
    if data:
        log_content = str(data)
        write_log(LOG_FILE_PATH, log_content)
        formatted_log_content = json.dumps(data, indent=2)

        prompt = f"""
        <instructions>
        Please analyze the following logs independently of any prior context.
        This request should be treated as an isolated case, without considering any previous sessions or data.
        If Fluent Bit is operating normally, do not mention it.
        If there are issues with parsing or analyzing the log data, report this explicitly.
        Summarize any other significant system activities in 5 to 6 sentences.
        The response must be in JSON format, with the keys 'summary' and 'severity'.
        Please return the JSON object directly, without wrapping it in code blocks or adding any additional text.
        The format should be:
        {{
            "summary": "Provide a concise overview of the significant events in the logs.",
            "severity": "Indicate the overall severity as either 'Info' or 'warn' based on the most critical log entry."
        }}.
        </instructions>

        <log_data>
        {formatted_log_content}
        </log_data>
        """

        llm_request_payload = {
            "model": "gemma2",
            "stream": False,
            "messages": [{"role": "user", "content": prompt}]
        }
        write_log(SENDLLM_LOG_FILE_PATH, json.dumps(llm_request_payload, indent=2))
        threading.Thread(target=send_to_llm, args=(llm_request_payload,)).start()

    return 'Logs received and processed', 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

■ Client

さてServer側は出来たので、/var/log/syslogを投げるClient側の準備をしていきます。 ログアグリゲーターは fluentbit にしてみました、以下1行でインストールは完了します(公式ドキュメントに従ってください)。

curl https://raw.githubusercontent.com/fluent/fluent-bit/master/install.sh | sh

設定例です。実際は1ファイルでなく複数のログファイルを一度にドバっと投げると要約のありがたさが出てくるんですが検証がめんんがんぐ・・・またの機会にします。

/etc/fluent-bit/fluent-bit.conf

[SERVICE]
    flush        60
    Daemon       Off
    Log_File     /var/log/fluent-bit.log
    Log_Level    debug

[INPUT]
    Name              tail
    Path              /var/log/syslog
    Tag               syslog
    Refresh_Interval  60
    DB                /var/log/flb_syslog.db
    Skip_Long_Lines   On

[OUTPUT]
    Name        http
    Match       syslog
    Host        app.pyが動いているサーバーのホスト名もしくはIPアドレス
    Port        5000
    URI         /receive_logs
    Format      json
    tls         Off
    Retry_Limit 3

では新しい設定で起動してみます。

sudo systemctl restart fluent-bit.service

■ Warningテスト

安定運用されているとテストにならないので以下のように危なそうなログを出してみます。 fluentbitを実行しているホストで以下を実行します。

logger -p user.warn "Disk space on /dev/sda1 is critically low: only 5% remaining."

すると以下のようなログがDiscordに飛びます、いかがでしょうか?

■ 終わりに

生成AIによる危ない状態だったら通知する今回の件はどうだったでしょうか。

 今回はsyslogの1ファイルを1分単位でチェックするというフローを試みました。これは、現在使用しているGPUの処理時間がそれほど速くないためです。しかし、本来の運用としては、30秒や15秒といったより短い間隔でチェックする方が望ましいでしょう。さらに、Context Windowの許容範囲内で複数台の複数のログを一つにまとめて送信することで、システム全体をより詳細に監視し、異常を早期に検知できる可能性が高まります。

 具体的にはOpenstackやkubernetesのようにシステムは1つだけどログファイルが複数ある場合、時分割的に生成AIに状況をサマってもらうことで、主要ではない箇所の障害や注意する部分の目星がつきやすくなることでしょうか。大きなシステムにこそ、有用な仕組みのように思います。 あとは新しいアプリを試すときに、ログに目が慣れていないときもまとめてもらえると非常に助かるので、ログのまとめは今後重要になっていくのかな、と感じています。

 ただし、この手法にはいくつかの課題もあります。まず、GPUを常に稼働させるため、電気代が増加する点です。また、GPUのRAM容量が小さい場合、試せるモデルが限られてしまうという問題もあります。

 電気代の問題については、トラブルシューティングを行わない(寝てる間とかの)時間帯にはFluent Bitを停止させる、もしくはクラウドベースのGPT-4o miniやBedrockのHaikuを利用することで、電気代よりも低コストで運用することが可能かもしれません(が、間違えて毎秒送ってしまうとかポカミスすると逆に高くつくのでご注意を)。

 RAMについては会社PCをNVIDIA付きにしてもらう、もしくは16GB程度のGPUを会社に買ってもらうなどで、色々なモデルを試してみていくと生成AIと仲良くなれやすい・・・というのが遊んで感じた感想です←遠回しにおねだりしてみるw

という感じでそれではまた。