ある研究者の手記

セキュリティとかゲームとかプログラミングとかそのへん

Amazon S3上のログのスキーマを管理していい感じに使うGo言語用ツールを作った

あまりにニッチすぎるから全く需要ないかなと思ったんですが、世の中に3人くらいは同じ悩みを抱えた人がいるかなー?と思ったので一応ブログをしたためておきます。

github.com

これはなに

タイトルでちょっと雑に書きましたが、もう少し正確言いうとAmazon S3上に保存したログのフォーマットやスキーマの一部をコード上で定義し、管理するためのフレームワーク in Go言語です。以下のように定義を書いた後、S3のバケット名、キーを指定するとパース済みの構造体を返してくれます。ここでは1行ずつのJSON形式で保存されたログが your-bucket バケットhttp_log/ 以下に入っており、さらに ts というフィールドに時刻情報が入っていることを定義しています。

   pipeline := rlogs.Pipeline{
        Ldr: &rlogs.S3LineLoader{},
        Psr: &parser.JSON{
            Tag:             "ts",
            TimestampField:  rlogs.String("ts"),
            TimestampFormat: rlogs.String("2006-01-02T15:04:05"),
        },
    }

    reader := rlogs.NewReader([]*rlogs.LogEntry{
        {
            Pipe: pipeline,
            Src: &rlogs.AwsS3LogSource{
                Region: "ap-northeast-1",
                Bucket: "your-bucket",
                Key:    "http_log/",
            },
        },
    })

実際の読み取りはこんな感じ。

   ch := reader.Read(&rlogs.AwsS3LogSource{
        Region: "ap-northeast-1",
        Bucket: "your-bucket",
        Key:    "http_log/log.json",
    })

    for q := range ch {
        if q.Error != nil {
            log.Fatal(q.Error)
        }
        values := q.Log.Values.(map[string]interface{})
        fmt.Printf("[log] tag=%s time=%s src=%v\n", q.Log.Tag, q.Log.Timestamp, values["src"])
    }

rlogsの中には自分自身の需要によりJSONVPC FlowLogs、CloudTrailのそれぞれのログに対応するパーサーを用意していますが、自分で実装することもできます。

なんで必要なのか

S3はオブジェクトストレージという性質上、当然ながら保存するときのログの中身・構造は一切問われません。そのためフォーマットやスキーマという概念を(ほぼ)無視してログの保存ができますが、一方でログを利用する際には別途中身をパースする必要があります。

保存しているログが2、3種類だけしかない、あるいは保存する際にちゃんと共通のスキーマに落とし込んでいる、というような場合はこれ以上お話することはないのですが、だいたいの場合現実はそうはいかないかなと思っています。特にS3にログを保存するときのメリットは「高可用でログの一次保存ができる」という点が大きいです。そのため、ログを保存する前にあれこれやってエラーなどがおきて可用性が下がってしまうというのは、アーキテクチャの旨味が少なくなってしまうかなと思います。

ということからログを利用する際にはどのバケット、どのプレフィクスにどんなログが入っているかを知っている必要があります。ログを利用するツール・システムが1つだけならそこに定義などを書いておけばいいのですが、実際には様々なことにログを利用したくなるため、自分は共通化したいと考えました。そこで共通利用できるようにして、ついでにログの取得+パースの機能もつけようと思ったのがこのフレームワークです。

どう使うのか

組織内などで1つレポジトリを作り、そこにフォーマットやスキーマの定義を書いて、それをGoのパッケージとして呼び出す、というような使い方を想定しています。例えば your-git-server.example.com のようなサーバがあって your-git-server.example.com/someone/logparser のようなレポジトリを用意した場合、以下のような感じで定義を書いておきます。

package logparser

func NewReader() *rlogs.Reader {
        logEntries := []*rlogs.LogEntry{
                // ログの取り込み方+パースの仕方+場所の定義を
                {
                        Pipe: rlogs.Pipeline{
                                Ldr: &rlogs.S3LineLoader{},
                                Psr: &parser.JSON{
                                        Tag:             "your.log",
                                        TimestampField:  rlogs.String("time"),
                                        TimestampFormat: rlogs.String("2006-01-02T15:04:05Z"),
                                },
                        },
                        Src: &rlogs.AwsS3LogSource{
                                Region: "ap-northeast-1",
                                Bucket: "your-backet",
                                Key:    "logs/",
                        },
                },
               // いろいろ書く
        }
        return rlogs.NewReader(logEntries)
}

そのあと、実際にログを利用するコード内で以下のように呼び出します。

package main

import (
        "your-git-server.example.com/someone/logparser"
)

func main() {
    reader := logparser.NewReader()
    ch := reader.Read(&rlogs.AwsS3LogSource{
        Region: "some-region",
        Bucket: "your-bucket",
        Key:    "http/log.json",
    })

    for q := range ch {
        if q.Error != nil {
            log.Fatal(q.Error)
        }
        values := q.Log.Values.(map[string]interface{})
        fmt.Printf("[log] tag=%s time=%s src=%v\n", q.Log.Tag, q.Log.Timestamp, values["src"])
    }

}

どう使っているか

以前、会社のブログにも書いたですが、今の仕事ではおよそ20種類ぐらいのログをこのフレームワークを利用して取り込み、いろいろなことに利用しています。このあたりの話はまたいずれ改めて会社のブログなどに書くかもしれません。