Dockerのログをlogglyに集約して見やすくする

大量のコンテナ（開発・本番含めると100超えそう）を抱えたDockerのログ管理をlogglyにかえてみたらだいぶ見やすくなったというお話です。

最終的には、Dockerのログだけではなく、Linuxのsyslogやnginxを含めてほぼ全てのログをlogglyに送るようになりました。

# 前提

## なぜlogglyか

logglyは簡潔に言うと**kibanaをログ参照用に使いやすくした**です。
ログをためているのがElasticsearchだからです。

https://www.loggly.com/blog/loggly-chose-elasticsearch-reliable-scalable-log-management/


検索画面はこんな感じです。

![Loggly___Search_and_Slack_-_wacul.png](https://qiita-image-store.s3.amazonaws.com/0/63168/1ce49430-1dca-d7a6-7442-704735dd0711.png)

インデックスされた対象は、左のタブから一覧できるようになっており、クリックすると検索のフィルタとして追加されます。当然検索フィールドから検索することも可能です。

そこで、タグに環境名、appNameにサービス名をいれています。そのためtag:から環境を絞って、appNameに表示されるサービスの一覧から探すといったことができます。

また、msgidにコンテナIDをいれているので、サービスごとに複数のコンテナが立っていたりしているようなログでも分けて参照できます。

さらに、この検索フィールドには特定の条件に沿ってパースした結果を放り込むことができるので、ログ中の `level` をインデックス化させることで、syslog形式によらないエラーレベルのインデックスを一覧させるようなことができます。

![Loggly___Search.png](https://qiita-image-store.s3.amazonaws.com/0/63168/26e64a37-2f0a-47c2-d7b5-5a2890e6382c.png)


kibanaほど詳細ではありませんが、普通に使うのには十分なグラフ化機能があります。

![Loggly___Search.png](https://qiita-image-store.s3.amazonaws.com/0/63168/c7719533-3e11-daa6-ec1f-298e348c2f3c.png)


当然ながら、検索条件からアラート送信することが可能です。
送信先はいくつかありますが、slackに送ることができます。

## 以前の環境

以前は papertrail + kibana を利用していました。
が、papertrailはログが少ないうちはよいのですが、数が多くなってくるとログの振り分けや検索性にかなり難がありました。
そのため、補助としてkibanaを追加していたのですが、機能としては十分なんですが検索性と可読性がよろしくないという問題を抱えてました。

他に、ログ集約のツールとしては競合にlogentriesがいますが、ログの検索性に難があり、タグ付けて解決しようにも、dynamicなタグ付けもうまくないようなのでちょっと触っただけで候補から外しました。

# 概要

Dockerのsyslog driverを使ってrsyslog経由でlogglyにログを送り出します。ログの送信にはTLSが使えます。

syslog driverを使ってしまうと、docker logsが使えませんが、前述の通りコンテナが大量だとどうせdocker logsで見る気はしないので、間に余計なものを入れたくないという理由です。
docker logsを使う場合はlogsproutなりで取得し、rsyslog経由で送るのがよいかと思います。

logglyはお試し期間でほぼ全機能を試せます。

# rsyslog

https://www.loggly.com/docs/rsyslog-tls-configuration/

送信のしかたは公式にあります。まとめれば

* rsyslog-gnutlsがなければ入れる
* certificatesをダウンロードする
* omfwdでlogglyサーバにTLSで送信する

送信するときのメッセージに、アカウントごとに発行されるトークンを入れることでユーザを判別しているようです。
手順どおりにやれば普段syslogで取られているログを送ることができます。

ここで、できれば最新のrsyslogを公式から取得してインストールすることをお勧めします。Amazon Linuxだとv5なのですが、最新のrsyslogではもっとプログラムっぽく設定を記載することができるため、Dockerのログを送るときにこれを活用するためです。
さらに、imfileでファイルからデータを取得するのに、pollingではなくinotifyを使えるようになっています。

logglyに送信するためのtemplateとaction以外の基本的な設定は、rsyslog.conf本体に書いておいたほうが、他のデータをlogglyに送る設定の読み込み順の都合上いいでしょう。


以下に送信に使っっているtemplateとactionの例を記載しておきます。moduleの読み込みなどはrsyslog.confで済ませているという前提です。


## Docker

現在使っているのが下記のようなテンプレートを使っています。TOKENとなっているところにトークンが入ります。

dockerを起動するときのオプションとして `--log-driver syslog --log-opt tag=docker/ENVIRONMENT/ROLE/{{.ID}}` とします。ENVIRONMENTに環境名、ROLEはサービス名、 `{{.ID}}` はdockerがコンテナIDを入れてくれます。

```/etc/rsyslog.d/10-docker.conf
template(name="DockerLoggly" type="list") {
    constant(value="<")
    property(name="pri")
    constant(value=">")
    property(name="protocol-version")
    constant(value=" ")
    property(name="timestamp" dateFormat="rfc3339")
    constant(value=" ")
    property(name="hostname")
    constant(value=" ")
    property(name="$!environment")
    constant(value="-")
    property(name="$!role")
    constant(value=" ")
    property(name="procid")
    constant(value=" ")
    property(name="$!containerid")
    constant(value=" [TOKEN@41058 tag=\"docker\"")
    constant(value=" tag=\"")
    property(name="$!environment")
    constant(value="\"")
    constant(value="] ")
    property(name="$!multilinemessage")
    constant(value="\n")
}

template(name="DockerLogFile" type="list") {
    constant(value="/var/docker/log/")
    property(name="$!environment")
    constant(value="/")
    property(name="$!role")
    constant(value="/")
    property(name="$!containerid")
    constant(value=".log")
}

#Script below will send  'docker' in programname.
if ($programname == 'docker')
then
{
    set $!environment = field($syslogtag, "/", 2);
    set $!role = field($syslogtag, "/", 3);
    set $!containerid = field(field($syslogtag, "/", 4), "[", 1);

    set $!multilinemessage = replace($msg, '\\n', '\n\t');

    action(type="omfile" DynaFile="DockerLogFile")
    action(type="omfwd" protocol="tcp" target="logs-01.loggly.com" port="6514" template="DockerLoggly" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" StreamDriverPermittedPeers="*.loggly.com")
    stop
}
```

そうすると、`$syslogtag`には `docker/ENVIRONMENT/ROLE/コンテナID[procid]` 、`$programname` には `docker` が入るようになります。rsyslogが持つpropertyは下記に記載されています。

http://www.rsyslog.com/doc/master/configuration/properties.html

上記コンフィグでは、`$programname` に `docker` があったらdockerのログだと判定し、`syslogtag` からログにしたい値を取り出して、おまけに `\n` という文字列を改行+タブにしてmultilineにしてあげてから、ファイルに書き出しとlogglyへ送信を行い、ログを捨てます。

注意点としては、一括してlogglyに送っている `22-loggly.conf` より後に読み込まれることがないようにしなければいけません。でないと、二重に送信されてしまいます。

## nginx


nginx側が吐き出すログはlogglyがautoparseしてくれるものから選んで、一つのファイルに出力するようにしています。

https://www.loggly.com/docs/automated-parsing/#nginx

```/etc/rsyslog.d/12-nginx.conf
input(type="imfile" tag="nginx" file="/var/log/nginx/loggly_access.log" Severity="info")
input(type="imfile" tag="nginx" file="/var/log/nginx/error.log" Severity="error")

template(name="LogglyNginxFormat" type="string"
    string="<%pri%>%protocol-version% %timestamp:::date-rfc3339% %HOSTNAME% %app-name% %procid% %msgid% [TOKEN@41058 tag=\"nginx\" tag=\"ENVIRONMENT\"] %msg%\n"
)

# Send to Loggly then discard
if ($programname == 'nginx') then
{
    action(type="omfwd" protocol="tcp" target="logs-01.loggly.com" port="6514" template="LogglyNginxFormat" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" StreamDriverPermittedPeers="*.loggly.com")
    stop
}
```

## mongov3

rsyslogが結構無茶をできる例として。

logglyはmongov3の形式のログをautoparseしないのですが、それを書き換えた上で、syslogのserverityをmongoが出すものに置き換えるということをやっています。
またACCESSのログは邪魔なので送ってません。

```
input(type="imfile" tag="MongoDB" file="/var/log/mongodb/mongod.log")

template(name="LogglyMongoFormat" type="string"
    string="<%!mongopri%>%protocol-version% %timestamp:::date-rfc3339% %HOSTNAME% %app-name% %procid% %!component% [TOKEN@41058 tag=\"MongoDB\"] %!mongotime% %!mongomessage%\n"
)


# Send to Loggly then discard
if ($programname == 'MongoDB') then
{
    set $!severity = re_extract($msg, "^([^ ]+) ([FEWID]) ([a-zA-Z0-9]+) +(\\[.+)", 0, 2, "");
    set $!component = re_extract($msg, "^([^ ]+) ([FEWID]) ([a-zA-Z0-9]+) +(\\[.+)", 0, 3, "");
    set $!mongotime = re_extract($msg, "^([^ ]+) ([FEWID]) ([a-zA-Z0-9]+) +(\\[.+)", 0, 1, "");
    set $!mongomessage = re_extract($msg, "^([^ ]+) ([FEWID]) ([a-zA-Z0-9]+) +(\\[.+)", 0, 4, "");
    set $!mongopri = 133;
    if ($!severity == 'F') then {
        set $!mongopri = "130";
    } else if ($!severity == 'E') then {
        set $!mongopri = "131";
    } else if ($!severity == 'W') then {
        set $!mongopri = "132";
    } else if ($!severity == 'I') then {
        set $!mongopri = "134";
    } else if ($!severity == 'D') then {
        set $!mongopri = "135";
    }

    if ($!component == 'ACCESS') then {
        stop
    }
    action(type="omfwd" protocol="tcp" target="logs-01.loggly.com" port="6514" template="LogglyMongoFormat" StreamDriver="gtls" StreamDriverMode="1" StreamDriverAuthMode="x509/name" StreamDriverPermittedPeers="*.loggly.com")
    stop
}
```


# 終わりに

まだ使い始めて間もないですが、ログの検索性がかなり向上したと感じています。

いまのところPro Lisenceで利用していますが、Enterprise Lisenceだとanomaly detection、live tailなどの機能があるようです。


不満な点を挙げるとすれば

* グラフを保存してdashbboardに表示できるが、時間の幅がグラフ化したときの時間範囲で固定されて選べない。1h前のグラフとして保存すると、24h前のものは別のグラフで取る必要がある。
* アラートでくる時刻表示がUTC固定で選べない
* Derived Fields(文字をパースしてインデックスする機能)のkey-valueの区切り文字としてタブが使えない。
* Derived Fieldsの最大数がPro Lisenceだと50項目まで（買え）。