hotch-potch, Note to self

いろいろ作業記録

リモートのsyslogにログを送信

1.はじめに

Elixirの動作ログをsyslogに送信します。

localhostに向けて送信するには、既存の下記ライブラリが使えます。

今回はリモートホストに送信する必要があったのですが、上記はリモートホストに対する設定がなかったり、そのほかリモートホスト対応のライブラリが見つからない(よくわからない)ので、syslogのプロトコルを自分で実装して、UDP送信するサンプルを作ってみました。

2.環境

$ uname -a
Linux myhost 6.2.0-26-generic #26~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Jul 13 16:27:29 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

$ elixir -v
Erlang/OTP 24 [erts-12.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit]

Elixir 1.13.4 (compiled with Erlang/OTP 24)

3.rsyslogdの設定変更

(1)既存の設定をバックアップ

$ sudo cp -p /etc/rsyslog.conf /etc/rsyslog.conf.original

(2)設定を変更

$ sudo nano /etc/rsyslog.conf

16行目付近のコメントを外す。 UDP

修正前

# provides TCP syslog reception
#module(load="imtcp")
#input(type="imtcp" port="514")

修正後

# provides UDP syslog reception
module(load="imudp")
input(type="imudp" port="514")

(3)設定の反映

$ sudo systemctl restart rsyslog.service 

$ systemctl status rsyslog.service 

(4)送信テスト、受信内容の確認

$ logger "hello" -i -t "sample" -n localhost

$ tail -10 /var/log/syslog
…
Jul 31 16:42:44 myhost sample[507666] hello
…

4.Elixirでの実装

(1)syslogプロトコル

goでの実装例を参考にしました。

golangでsyslogパケットを送る. 時にはsyslogサーバーのテストのためにsyslogパケットを送りたい時がある… | by sushiqueue | Medium

考え方

任意のホストのポート514に対し、下記の文字列をUDP(あるいはTCP)で送信

<priority>timestamp hostname tag: message

記号 内容 補足
priority 計算式:(facility << 3) | severityで算出 <>で囲むこと
timestamp タイムスタンプ フォーマット例:Aug 27 22:09:46
hostname 送信元ホスト名
tag 送信元のアプリケーション名 後述のサンプルではアプリの[PID]を付加
message メッセージの本文

(2)実装例

プロジェクト

$ mix new syslogr
$ cd syslogr

#新たにソースファイルを作成
$ touch ./lib/rsyslog.ex

lib/rsyslog.ex

下記を記述

defmodule Rsyslog do
  @moduledoc """
  Documentation for `rsyslog`.
  """

  # 自分のホストの任意のポート番号
  @portclient 32154
  # タイムゾーン(ログの時刻に必要)
  @timezone "Japan"

  # 送信先ホスト(シングルクォートで囲むこと)
  # 例ではlocalhostですが、リモートでもOKです
  @host '127.0.0.1'
  # 送信先ポート
  @port 514

  defp build_message(message, hostname, facility, severity)
       when is_bitstring(message) 
              and is_bitstring(hostname)
              and is_integer(facility)
              and is_integer(severity) do

    # 時刻を指定のフォーマットに書き出し
    {:ok, timestring} =
      Timex.now(@timezone)
      |> Timex.format("{Mshort} {0D} {h24}:{m}:{s}")

    # 当アプリ名を取得
    {:ok, tag} = :application.get_application(__MODULE__)

    # syslog用文字列の組み立て
    "#{priority(facility, severity)}#{timestring} #{hostname} #{tag}[#{System.pid()}]: #{message}"
  end

  defp priority(facility, severity)
       when is_integer(facility) and is_integer(severity) do
    # priorityの値の計算と文字列生成

    # ルールに従いフォーマット
    use Bitwise
    "<#{inspect(facility <<< 3 ||| severity)}>"
  end

  @doc """
  ログの送信

  ## Parameters
    - message : 送信データ
    - host    : 送信先ホスト
    - port    : 送信先ポート
    - facility: 16 (local use 0 (local0)をデフォルトとしています)
    - severity: 6 (Infoをデフォルトとしています)

  ## Examples
    iex> Rsyslog.log("hoge")
  """
  def log(message, severity \\ 6, host \\ @host, port \\ @port, facility \\ 16)
      when is_bitstring(message)
              and is_integer(facility)
              and is_integer(severity)
              and is_list(host) and is_integer(port) do
    # ホスト名の取得
    {:ok, hostname} = :inet.gethostname()

    # 送信データの生成
    mes = build_message(message, "#{hostname}", facility, severity)

    # 送信
    {:ok, socket} = :gen_udp.open(@portclient, [:binary])
    response = :gen_udp.send(socket, host, port, mes)
    :gen_udp.close(socket)
    {response, mes}
  end
end

mix.exs

Timexを追加

# Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
      {:timex, "~> 3.7"}
    ]
  end

(3)送信テスト

ビルド

$ mix deps.get
$ iex -S mix

送信

iex> Rsyslog.log("hoge")
{:ok, "<134>Aug 27 22:09:46 myhost  syslogr[159869]: hoge"}

ログの状態

$ tail -10 /var/log/syslog
…
Aug 27 22:09:46 myhost syslogr[160578]: hoge

参考資料