■ はじめに
こんにちは。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
という感じでそれではまた。