2024年2月3日土曜日

LangChain の Retrieval Chains を使って FAISS で検索した類似度情報を元に LLM で回答させるようにしてみる (create_retrieval_chain ver)

LangChain の Retrieval Chains を使って FAISS で検索した類似度情報を元に LLM で回答させるようにしてみる (create_retrieval_chain ver)

概要

LangChain には事前に作成したデータを元に LLM (ChatGPT や AzureOpenAI)を使って回答を作成することができる機能があります
今回は FAISS で作成した類似度検索結果を元に LLM を使って回答を作成する Chains を使う方法を紹介します (これを Retrieval Chain と呼びます

環境

  • macOS 11.7.10
  • Python 3.11.6
  • langchain 0.1.4
    • langchain-openai 0.0.5
    • langchain-community 0.0.17
    • sentence-transformers 2.3.1
    • faiss-cpu 1.7.4
    • beautifulsoup4 4.12.3

サンプルコード

コード中のコメントに詳しい処理の内容を記載しています
ポイントは faiss で作成したベクトル情報を Retrieval に変換し LLM にチェインしている流れになります

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI


# AzureOpenAI を LLM として利用する、LLM には最終的に faiss で検索した結果を元に回答を作成してもらう役割
llm = AzureChatOpenAI(
    api_key="xxx",
    api_version="2023-05-15",
    azure_endpoint="https://my-resource.openai.azure.com",
)

# ベクトル化するテキストを Web 上から取得、ここはどんな情報でも OK
# 特定の回答に特化させることができるような情報を使用する
# 今回は WebBaseLoader を使っているが他にも直接テキストを与えたり PDF の情報を与えたりすることができる
loader = WebBaseLoader("https://docs.smith.langchain.com/overview")
docs = loader.load()

# HuggingFaceEmbeddings を使って HuggingFace にあるモデルを使ってテキストをベクトル化する
# ここも Embeddings する際は他の Embeddings 機能でも OK (OpenAI Embeddings など
embeddings = HuggingFaceEmbeddings()
text_splitter = RecursiveCharacterTextSplitter()
documents = text_splitter.split_documents(docs)
vector = FAISS.from_documents(documents, embeddings)

# ベクトル情報の保存と読み込み (一度保存したら二回目以降はロード部分から実行してもOK
vector.save_local("./vectorstore")
vector = FAISS.load_local("./vectorstore", embeddings)

# チャットプロンプトを作成、この {context} の部分に faiss からの結果を与えることで faiss の結果を考慮した回答を作成してもらう
prompt = ChatPromptTemplate.from_template(
    """Answer the following question based only on the provided context:

<context>
{context}
</context>

Question: {input}"""
)

# create_stuff_documents_chain でチャットプロンプトで作成した問い合わせ文言を使って LLM に問い合わせるようにする
document_chain = create_stuff_documents_chain(llm, prompt)

# document_chain を直接使う場合は以下のようにします (今回は更に faiss と連携した結果を使って LLM に問い合わせるのでコメントアウトしています
# from langchain_core.documents import Document
#
# response = document_chain.invoke(
#     {
#         "input": "how can langsmith help with testing?",
#         "context": [
#             Document(page_content="langsmith can let you visualize test results")
#         ],
#     }
# )
# print(response)

# ベクトル化情報を Retrieval に変換します https://python.langchain.com/docs/modules/data_connection/
retriever = vector.as_retriever()

# そしてチャットプロンプト+LLMのチェインに更に Retrieval をチェインすることで faiss の結果を元にチャットプロンプト -> LLM という流れで問い合わせできるようになります
retrieval_chain = create_retrieval_chain(retriever, document_chain)

# 問い合わせ、結果がちゃんと Web から取得した情報を加味した回答になっていることを確認します
response = retrieval_chain.invoke({"input": "how can langsmith help with testing?"})
print(response["answer"])

ちょっと解説

今回は 2 つの Chains を使っています

  • create_stuff_documents_chain
  • create_retrieval_chain

前者は LLM + チャットプロンプトを連携する Chains で LLM への入力にテキストを入力するのではなくテンプレート経由で生成したテキストを入力にしています

後者はテンプレート経由で生成されたテキスト + faiss の連携をしています
faiss で類似度検索結果された情報を元にテンプレートで入力テキストを作成しそれを LLM に投げています

こんな感じで Chains はローカルで実施、加工した情報を元に LLM に質問を投げるといってことができるツールになります (他にもいろいろな連携ができます

最後に

LangChain の Retrival Chain を使って faiss の結果を元に LLM に回答を作成してもらうサンプルを紹介しました
Web 上には同じような手法で RetrievalQA と呼ばれる Chain を使った手法がよく紹介されていますが RetrievalQA は Legacy Chain と呼ばれており古い手法でそのうちなくなるので今回紹介した create_retrieval_chain を使うようにしましょう

LLM をそのままにあたかも専用のモデルを作成したかのような回答を得ることができます
ファインチューニングのようにモデルを専用にビルドする必要がなくマシン側でベクトル環境を準備するだけでカスタムできるのが良い点かなと思います

ただ逆にベクトル化データベースなどの準備必要になるのでそれはデメリットでもあります

参考サイト

2024年2月2日金曜日

Python で selenium を動かすサンプルコード

Python で selenium を動かすサンプルコード

概要

現状の環境で動作するのか確認したかったのでメモ
Chrome は起動中のウィンドウを使います

環境

  • macOS 11.7.10
  • chrome 120.0.6099.216
  • chromedriver 120.0.6099.109

chromedriver

  • brew install chromedriver

インストール

  • pipenv install selenium

chrome 起動

  • "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222

サンプルコード

特定のサイトの商品IDの一覧を先に取得しその後 ID を元に商品のリンク先に飛びリンク先のテキスト情報を表示する流れです

(どうしても安定しないところは sleep やリトライを入れるしかないのだろうか)

import time

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

# 既存chromeを起動
chromeoptions = Options()
chromeoptions.debugger_address = "127.0.0.1:9222"
driver = webdriver.Chrome(options=chromeoptions)


def init():
    driver.get("https://your.target.site")
    driver.find_element(By.XPATH, '//*[@id="js-list"]/div/a').click()


def init_ids() -> list:
    init()
    ids = []
    products = driver.find_elements(By.XPATH, '//*[@id="js-list"]/ul/li/div')
    for p in products:
        id = p.get_attribute("id")
        ids.append(id)
    return ids


def show_product_info(id: str):
    init()
    driver.find_element(By.XPATH, f'//div[@id="{id}"]').click()
    time.sleep(2)
    driver.find_element(By.XPATH, '//*[@id="js-product-tabs"]/div[1]/cite/a').click()
    try:
        driver.find_element(By.XPATH, '//*[@id="a_performer"]').click()
    except NoSuchElementException:
        pass
    print(driver.find_element(By.XPATH, '//*[@id="title"]').text)
    print(driver.find_element(By.XPATH, '//*[@id="performer"]').text)
    # 戻る
    driver.back()


def close():
    driver.close()


if __name__ == "__main__":
    ids = init_ids()
    for id in ids:
        show_product_info(id)
    close()

最後に

selenium は相変わらずDOMの表示タイミングなどがシビアなので安定して動かすのが大変な印象です
仕方なくブラウザからしか情報が取得できない場合を除きできる限り selenium は使わないほうがいいと思います

個人のツールとしてならありかなと思います

参考サイト

2024年2月1日木曜日

Keras で mnist の画像学習を CNN を使ってやってみる

Keras で mnist の画像学習を CNN を使ってやってみる

概要

前回 Keras + mnist のチュートリアルをやりました
今回は CNN を使った方法を紹介します
チュートリアルはこちらになります

勉強のために Conv2D のレイヤーを増やしたバージョンも紹介します

環境

  • macOS 14.2.1
  • Python 3.11.6
  • tenforflow 2.15.0
  • keras 2.15.0

サンプルコード

import numpy as np
import tensorflow as tf

mnist = tf.keras.datasets.mnist
# 60000個の学習データ
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 学習データとテストデータを3次元から4次元に変換、Conv2D を使う場合は大抵の場合4次元必要、前回 https://hawksnowlog.blogspot.com/2024/01/keras-mnist-tutorial.html Dense のみは3次元のデータのままいける
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
# ピクセル情報は0-255の範囲なので255で割ることで0-1の範囲に変換する
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),  # 3x3 の複数のフィルタを32枚使って画像にフィルタをかけ新たな行列を作成
    tf.keras.layers.MaxPooling2D((2, 2)),  # 作成した新たな行列に対して 2x2 の領域で最大の値を取得、更にそれを新たな行列とする
    # ここで更に Conv2D + MaxPooling2D のレイヤーを増やしても OK、ただし学習時間は長くなる
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    # ここから出力用のレイヤー
    tf.keras.layers.Flatten(),  # 3次元->1次元への変換
    tf.keras.layers.Dense(64, activation='relu'),  # 64ノードの全結合->10ノードの全結合
    tf.keras.layers.Dense(10, activation='softmax'),  # 最後はソフトマックスで10個ある出力の合計が1になるように変換する (定番
])

# モデルコンパイル
model.compile(optimizer='adam',  # 最適化関数は Adaptive moment
              loss='sparse_categorical_crossentropy',  # 損失関数は交差エントロピー誤差
              metrics=['accuracy'])  # 評価関数は正解率

# # 学習
model.fit(x_train, y_train, epochs=5)


# 評価
model.evaluate(x_test, y_test)
# 予測と結果表示
results = model.predict(x_test[:5])
# 結果はDense(10)なので10個の配列の予測値から成り立っている
# その配列10個中で一番値の大きいインデックスが予測した手書きの数字になる
# np.argmax はそれを一発でやってくれる便利メソッド
print(np.argmax(results, axis=1))  # [7 2 1 0 4] になるはず
print(y_test[:5])  # [7 2 1 0 4] こっちは答え

Conv2D のレイヤーを増やしてみる

前のコードのレイヤーを増やしてみます
最後の Conv2D(64, (3, 3), activation='relu') で (3, 3, 64) となりこれに更に conv2D でフィルタをかけると (1, 1, 128) などになり

ValueError: One of the dimensions in the output is <= 0 due to downsampling in conv2d_3. Consider increasing the input size. Received input shape [None, 1, 1, 128] which would produce output shape with a zero or negative value in a dimension.

というエラーになるので少しフィルタの次元数を調整します
具体的に (3, 3) を (2, 2) のフィルタにして次のレイヤに行く際の変換した画像の縦横の次元数を少しずつ減らすようにしています

import numpy as np
import tensorflow as tf

mnist = tf.keras.datasets.mnist
# 60000個の学習データ
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 学習データとテストデータを3次元から4次元に変換、Conv2D を使う場合は大抵の場合4次元必要、前回 https://hawksnowlog.blogspot.com/2024/01/keras-mnist-tutorial.html Dense のみは3次元のデータのままいける
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
# ピクセル情報は0-255の範囲なので255で割ることで0-1の範囲に変換する
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32, (2, 2), activation='relu', input_shape=(28, 28, 1)),  # 3x3 の複数のフィルタを32枚使って画像にフィルタをかけ新たな行列を作成
    tf.keras.layers.MaxPooling2D((2, 2)),  # 作成した新たな行列に対して 2x2 の領域で最大の値を取得、更にそれを新たな行列とする
    # ここで更に Conv2D + MaxPooling2D のレイヤーを増やしても OK、ただし学習時間は長くなる
    # 以下では 128 個のフィルタで Conv2D するレイヤーを増やしている
    tf.keras.layers.Conv2D(64, (2, 2), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(128, (2, 2), activation='relu'),  # レイヤーを増やす場合変換した行列の縦横の次元数が途中のレイヤで (1, 1) にならないように Conv2D のフィルタの次元数を調整する必要がある
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(128, (2, 2), activation='relu'),
    # ここから出力用のレイヤー
    tf.keras.layers.Flatten(),  # 3次元->1次元への変換
    tf.keras.layers.Dense(64, activation='relu'),  # 64ノードの全結合->10ノードの全結合
    tf.keras.layers.Dense(10, activation='softmax'),  # 最後はソフトマックスで10個ある出力の合計が1になるように変換する (定番
])


# モデルコンパイル
model.compile(optimizer='adam',  # 最適化関数は Adaptive moment
              loss='sparse_categorical_crossentropy',  # 損失関数は交差エントロピー誤差
              metrics=['accuracy'])  # 評価関数は正解率

# # 学習
model.fit(x_train, y_train, epochs=5)


# 評価
model.evaluate(x_test, y_test)
# 予測と結果表示
results = model.predict(x_test[:5])
# 結果はDense(10)なので10個の配列の予測値から成り立っている
# その配列10個中で一番値の大きいインデックスが予測した手書きの数字になる
# np.argmax はそれを一発でやってくれる便利メソッド
print(np.argmax(results, axis=1))  # [7 2 1 0 4] になるはず
print(y_test[:5])  # [7 2 1 0 4] こっちは答え

model.summary()

 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 27, 27, 32)        160       
                                                                 
 max_pooling2d (MaxPooling2  (None, 13, 13, 32)        0         
 D)                                                              
                                                                 
 conv2d_1 (Conv2D)           (None, 12, 12, 64)        8256      
                                                                 
 max_pooling2d_1 (MaxPoolin  (None, 6, 6, 64)          0         
 g2D)                                                            
                                                                 
 conv2d_2 (Conv2D)           (None, 5, 5, 128)         32896     
                                                                 
 max_pooling2d_2 (MaxPoolin  (None, 2, 2, 128)         0         
 g2D)                                                            
                                                                 
 conv2d_3 (Conv2D)           (None, 1, 1, 128)         65664     
                                                                 
 flatten (Flatten)           (None, 128)               0         
                                                                 
 dense (Dense)               (None, 64)                8256      
                                                                 
 dense_1 (Dense)             (None, 10)                650       
                                                                 
=================================================================
Total params: 115882 (452.66 KB)
Trainable params: 115882 (452.66 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

最後に

Keras + CNN で画像解析を試してみました
前回はただの Dense レイヤーだけでしたが今回は CNN という方法を使って判定しています
精度はどちらも良かったのですが画像解析の主流は CNN かなと思います
単純にレイヤーを増やすだけで精度がどんどんよくなるのが摩訶不思議な感じなしますがそれが DeepLearning のすごいところです

CNN 自体の説明は以下の動画が非常に参考になりました
https://www.youtube.com/watch?v=xzzTYL90M8s

参考サイト

2024年1月31日水曜日

Python の oepnai-whisper で Speech-to-text したので結果のメモ

Python の oepnai-whisper で Speech-to-text したので結果のメモ

概要

M2 mac mini 上で openapi-whisper を動かして音声解析してみました
音声ファイルが大きい場合は素直に mlx 経由で実行しましょう

環境

  • macOS 14.2.1 (M2 pro mac mini)
  • Python 3.11.6
  • openai-whisper 1.1.10
  • MLX 0.0.10

インストール

  • pipenv install openai-whisper

サンプルコード

import whisper

# モデルは large を使用
model = whisper.load_model("large")
# 解析
result = model.transcribe("/path/to/audio.m4a")
# ファイル書き出し
with open("result.txt", mode="w") as f:
    f.write(result["text"])

使用モデル

  • large
  • ファイルサイズ 2.88G

音声ファイル

  • m4a ファイル
  • サイズは 45.41MB
  • ビットレート 64kbps

解析時間

  • 不明 (かなりかかりそうなので途中でやめて mlx 形式にしました)

MLX 経由で実行する

以下は MLX を使って whisper を動作させる方法です
こちらを参考に設定していきます

clone

  • git clone https://github.com/ml-explore/mlx-examples.git

初期化

  • cd mlx-examples/whisper
  • brew install ffmpeg
  • pipenv install -r ./requirements.txt

モデルのコンバート

pytorch で作成された whisper のモデルを mlx で使用可能なモデルに変換します
もしかすると hugging face の MLX Community を使えば自分でコンバートする必要はないかもです

  • pipenv run python convert.py --torch-name-or-path large --mlx-path mlx_models/large
[INFO] Loading
[INFO] Saving

生成されるファイルは以下のとおりです

tree mlx_models
mlx_models
└── large
    ├── config.json
    └── weights.npz

2 directories, 2 file

スクリプト作成

  • vim app.py
import whisper

# 生成したモデルと音声ファイルを指定して解析
result = whisper.transcribe(
    "/path/to/audio.m4a", path_or_hf_repo="mlx_models/large", verbose=True
)

# ファイル書き出し
with open("result.txt", mode="w") as f:
    f.write(result["text"])

ここで import している whisper.transcribe は mlx-examples が独自で修正している transcribe.py になります
なので path_or_hf_repo というパラメータが指定できます

実行

  • pipenv run python app.py

今回の場合成功すれば result.txt にテキスト情報がそのまま保存されます

解析時間

  • 約1時間

GPU はほぼフルで使っていました
スペックによってはさらに速くなるかなと思います

最後に

mlx + whisper で M2 mac mini 上で音声解析してみました
精度はかなりいいので Google の Speech-to-text レベルのものがローカルで動作するようになります
スペックの低いマシンではかなり時間がかかるのでそれなりのスペックのマシン特に GPU のあるマシンを使って解析することをオススメします

また mlx を使うことで GPU をフルに使って解析するので時間も短縮できます

language='ja' というキーワード引数を指定しなくても自動で日本語だと判断してくれましたが入れてもいいのかもしれません
verbose=True を指定すると自動検出した言語を print デバッグしてくれるので試しに入れてみてもいいのかもしれません
また verbose=True は解析の経過も表示してくれるのでファイルが大きい場合は入れたほうがいいかなと思います

参考サイト

2024年1月30日火曜日

docker のログを logspout で集約する

docker のログを logspout で集約する

概要

logspout は docker のソケットファイルを使ってログを監視し監視しているログを別のログ集約システムに流すことができるツールです
監視するログはルールによって変更することができるため特定のログだけを特定の場所に飛ばしたりすることができます
今回はとりあえず動かすのが目的なので docker のすべてのログを logstash に流してみます

環境

  • macOS 14.2.1
  • docker 24.0.7
  • docker-compose 2.23.3
  • logspout 3.2.14

docker-compose.yml

logger は logstash コンテナで syslog を受取れるようにします
logspout は docker.sock を使い docker ログを監視します
今回の設定はすべてのログ (docker デーモン自体のログ) も監視対象です
web はデバッグ用のアプリになります

version: '3.8'
services:
  logger:
    image: logstash:8.12.0
    volumes:
      - ./logstash:/etc/logstash
    ports:
      - 5000:5000/udp
    environment:
      - XPACK_MONITORING_ENABLED=false
    command: logstash -f /etc/logstash/logstash.conf
  logspout:
    image: gliderlabs/logspout:v3.2.14
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock
    depends_on:
      - logger
    command: syslog://192.168.1.23:5000
  web:
    image: nginx
    ports:
      - "80:80"
    depends_on:
      - logspout

logstash.conf

syslog を受取りそれを標準出力します
ポイントは type = docker にする点です

input {
  syslog {
    port => 5000
    type => "docker"
  }
}

output {
  stdout {}
}

動作確認

  • docker-compose up -d

コンテナを起動しアプリにアクセスします
そして logger のログを見るとちゃんとアプリのログが logspout 経由で飛ばされているのが確認できると思います

  • docker-compose logs -f
  • curl localhost
web-1       | 192.168.65.1 - - [24/Jan/2024:05:53:36 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.4.0" "-"
web-1       | 192.168.65.1 - - [24/Jan/2024:05:53:54 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.4.0" "-"
logger-1    |        "message" => "<14>1 2024-01-24T05:53:36Z f9940f8079d8 docker-web-1 67205 - - 192.168.65.1 - - [24/Jan/2024:05:53:36 +0000] \"GET / HTTP/1.1\" 200 615 \"-\" \"curl/8.4.0\" \"-\"\n",
logger-1    |         "original" => "<14>1 2024-01-24T05:53:36Z f9940f8079d8 docker-web-1 67205 - - 192.168.65.1 - - [24/Jan/2024:05:53:36 +0000] \"GET / HTTP/1.1\" 200 615 \"-\" \"curl/8.4.0\" \"-\"\n"

特定のログだけフィルタするには

このままだとカーネルログも監視するので logger コンテナのログの量が大変なことになってしまいます
logspout は特定のログだけ監視することができる機能があり例えばコンテナ名で絞り込むには filter.name という機能を使います

logspout の command で filter.name を指定します
ワイルドカードで複数のコンテナ名で絞り込むことも可能です

これでカーネルのログは表示されなくなります

version: '3.8'
services:
  logger:
    image: logstash:8.12.0
    volumes:
      - ./logstash:/etc/logstash
    ports:
      - 5000:5000/udp
    environment:
      - XPACK_MONITORING_ENABLED=false
    command: logstash -f /etc/logstash/logstash.conf
  logspout:
    image: gliderlabs/logspout:v3.2.14
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock
    depends_on:
      - logger
    command: "syslog://192.168.1.23:5000?filter.name=*web*"
  web:
    image: nginx
    ports:
      - "80:80"
    depends_on:
      - logspout

最後に

logspout を試してみました
現状だと docker のロギングドライバで直接 fluentd や logstash に流すことができるのであまり使用する場面は少ないのかもしれません
またここ最近あまりメンテナンスもされていないため使用は控えたほうがいいのかもしれません

参考サイト

2024年1月29日月曜日

docker で gelf ドライバを使って logstash にログを送信してみる

docker で gelf ドライバを使って logstash にログを送信してみる

概要

過去に fluent-driver を使って fluentd にログを送信する方法を紹介しました
今回は gelf driver を使って logstash にログを送信してみました
なおコンテナは docker-compose で動作させます

環境

  • macOS 14.2.1
  • docker 24.0.7
  • docker-compose 2.23.3
  • logstash 8.12.0

docker-compose.yml

logstash と適当なアプリを起動します
ポイントはアプリ側の driver 指定で gelf を指定します
ここが logstash が起動するエンドポイントを指定する部分です

logstash は logger として docker-compose 内で指定します
12201:udp として起動します
logstash.conf はあとで紹介しますが今回は Elasticsearch などには連携せず受け取ったアプリからのログをそのまま標準出力するだけです

  • vim docker-compose.yml
version: '3.8'
services:
  logger:
    image: logstash:8.12.0
    volumes:
      - ./logstash:/etc/logstash
    command: logstash -f /etc/logstash/logstash.conf
    ports:
      - 12201:12201/udp
  web:
    image: nginx
    ports:
      - "80:80"
    logging:
      driver: gelf
      options:
        gelf-address: udp://192.168.1.23:12201
        tag: docker.web
    depends_on:
      - logger

logstash/logstash.conf

12201 で受け取ったログを標準出力します

  • vim logstash/logstash.conf
input {
  gelf {
    port => 12201
  }
}

output {
  stdout {}
}

動作確認

コンテナを起動します

  • docker-compose up -d

あとはログを確認しつつアプリにアクセスします
すると logstash 側に以下のようなログが出力されることが確認できます

  • docker-compose logs -f
  • curl localhost
logger-1  | {
logger-1  |               "host" => "docker-desktop",
logger-1  |           "image_id" => "sha256:6c7be49d2a11cfab9a87362ad27d447b45931e43dfa6919a8e1398ec09c1e353",
logger-1  |              "level" => 6,
logger-1  |            "version" => "1.1",
logger-1  |            "created" => "2024-01-24T00:28:59.739392089Z",
logger-1  |        "source_host" => "192.168.65.1",
logger-1  |           "@version" => "1",
logger-1  |     "container_name" => "docker-web-1",
logger-1  |         "@timestamp" => 2024-01-24T00:29:47.975Z,
logger-1  |         "image_name" => "nginx",
logger-1  |            "message" => "192.168.65.1 - - [24/Jan/2024:00:29:47 +0000] \"GET / HTTP/1.1\" 200 615 \"-\" \"curl/8.4.0\" \"-\"",
logger-1  |                "tag" => "docker.web",
logger-1  |            "command" => "/docker-entrypoint.sh nginx -g daemon off;",
logger-1  |       "container_id" => "2c997b2676e305f5fb7d29e49415c45a5cb9d7edcd6f73ad03d45124d158c246"
logger-1  | }

ElasticSearch に接続できないというエラーがずっと出る

以下のようなエラーがずっとログに出力されていました

logger-1  | [2024-01-24T00:35:42,235][ERROR][logstash.licensechecker.licensereader] Unable to retrieve Elasticsearch cluster info. {:message=>"No Available connections", :exception=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::NoConnectionAvailableError}
logger-1  | [2024-01-24T00:35:42,236][ERROR][logstash.licensechecker.licensereader] Unable to retrieve license information from license server {:message=>"No Available connections"}
logger-1  | [2024-01-24T00:35:42,752][INFO ][logstash.licensechecker.licensereader] Failed to perform request {:message=>"elasticsearch: Name or service not known", :exception=>Manticore::ResolutionFailure, :cause=>#<Java::JavaNet::UnknownHostException: elasticsearch: Name or service not known>}
logger-1  | [2024-01-24T00:35:42,753][WARN ][logstash.licensechecker.licensereader] Attempted to resurrect connection to dead ES instance, but got an error {:url=>"http://elasticsearch:9200/", :exception=>LogStash::Outputs::ElasticSearch::HttpClient::Pool::HostUnreachableError, :message=>"Elasticsearch Unreachable: [http://elasticsearch:9200/
][Manticore::ResolutionFailure] elasticsearch: Name or service not known"}

おそらく logstash の output に ElasticSearch の設定がないからだと思います
今回のようにテスト用途で ES に接続しないのであれば XPACK_MONITORING_ENABLED=false という環境変数を logstash に設定してあげると自動で接続しにいかなくなります

version: '3.8'
services:
  logger:
    image: logstash:8.12.0
    volumes:
      - ./logstash:/etc/logstash
    command: logstash -f /etc/logstash/logstash.conf
    environment:
      - XPACK_MONITORING_ENABLED=false
    ports:
      - 12201:12201/udp
  web:
    image: nginx
    ports:
      - "80:80"
    logging:
      driver: gelf
      options:
        gelf-address: udp://192.168.1.23:12201
        tag: docker.web
    depends_on:
      - logger

最後に

docker + logstash を連携してみました
基本的にはこのあとログを ElasticSearch に格納することになります
fluentd ではなく logstash を使いたい場合には gelf ドライバを使いましょう

参考サイト

2024年1月26日金曜日

iOS12 でネットワーク共有に接続できない場合の対処方法

iOS12 でネットワーク共有に接続できない場合の対処方法

対処方法

設定 -> 一般 -> リセット -> ネットワーク設定をリセット

で再度接続してみてください

環境

  • iPhone6 (12.5.7) 共有される側
  • iPhoneXR (17.12) 共有する側