退屈なことはGroovyにやらせよう

eiryu
67

はじめに

エンジニアの @eiryu と申します。
突然ですが、皆さんは Groovy というプログラミング言語はご存じでしょうか?JVM言語の1つであり、Javaの豊富なライブラリが使え、動的言語でもあるためちょっとしたスクリプトやツールを作るには便利です。
この記事では、実際に私が業務で作ったGroovyスクリプトについて紹介していきたいと思います。

環境情報

この記事の内容は以下の環境にて確認しています。

$ groovy -v
Groovy Version: 2.5.0 JVM: 1.8.0_181 Vendor: Azul Systems, Inc. OS: Mac OS X

$ awslogs --version
awslogs 0.10.0

業務で実際に使った例

awslogsを使ってCloudWatch Logsから並列でログを取得する

@GrabConfig(systemClassLoader = true)
@Grab('mysql:mysql-connector-java:5.1.31')
@Grab('joda-time:joda-time:2.9.9')
import groovy.sql.Sql
import groovyx.gpars.GParsPool
import org.joda.time.DateTime


class Config {
    static DB_HOST     = System.getenv().DB_HOST
    static DB_NAME     = System.getenv().DB_NAME
    static DB_USERNAME = System.getenv().DB_USERNAME
    static DB_PASSWORD = System.getenv().DB_PASSWORD
}

def db = Sql.newInstance("jdbc:mysql://${Config.DB_HOST}/${Config.DB_NAME}?useLegacyDatetimeCode=false", Config.DB_USERNAME, Config.DB_PASSWORD, 'com.mysql.jdbc.Driver')

db.withTransaction {
    maintain(db)
}

def maintain(Sql db) {
    def rows = db.rows('select * from targets')

    GParsPool.withPool {
        rows.eachParallel { row ->
            def reportId = row['report_id']
            def createdAt = new DateTime(row['created_at'])
            def start = createdAt.minusSeconds(1)
            def end   = createdAt.plusSeconds(2)

            println "ログ取得開始 reportId: ${reportId}"
            def log = "/usr/local/bin/awslogs get service-name/prod/api -S -G -s='${start}' -e='${end}'".execute().text
            db.executeInsert("""
                insert into logs(
                  report_id,
                  log
                ) values(
                  ${reportId},
                  ${log}
                )
            """)
            println "ログ取得完了 reportId: ${reportId}"
        }
    }
}

障害調査でDBのreportテーブルのレコードが出来た時のAPIのログが必要になった時がありました。
調査対象レコードの情報は事前にtargetsテーブルに準備しておき、その情報を元に awslogs コマンドを利用してCloudWatch Logsからログを取得しています。

先頭の @Grab は依存ライブラリを指定する記述1)Grape Annotationです。Groovyスクリプトでは、このように依存ライブラリの指定とスクリプトを同じファイルに記述することが出来ます。

Config classにはDBの接続情報を環境変数から取得するようにしています2)よくある並行性の問題を GPars で解決する。コード中に書けない情報や、S3のバケット名などパラメータ化して容易に差し替えたいものについてはこのようにします。 Twelve-Factor App でおなじみですね。

Groovyでは、 'command'.execute() で外部プロセス実行出来る3)Executing External Processesため、インストールされている他のコマンドと組み合わせた作業に便利です。

ログの取得は割と時間がかかるため、GParsというライブラリを使って並列に取得しています。eachParallel 部分がコレクションを取り出しながら並列処理をしている部分です4)よくある並行性の問題を GPars で解決する5)[Groovy]GParsで並列処理(基本&コレクション編)

別のスクリプトにて、調査対象のエンドポイントのログに絞るなど、詳細なログの分析をするためにここでは一旦logsテーブルにログの内容を保存しています。ログの内容は大きくなる可能性があるため、MySQLの場合はmax_allowed_packetを適切に設定しておくことが必要です。

AWSのALBログをパースして欲しい情報だけ抽出する

new File('/Users/eiryu/Downloads/alb.log').eachLine {
    def m = (it =~ '([^ ]*) ([^ ]*) ([^ ]*) (([^ ]*):([^ ]*)|-) (([^ ]*):([^ ]*)|-) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) \"([^\\\"]*)\" \"([^\\\"]*)\" ([^ ]*) ([^ ]*) ([^ ]*) \"([^\\\"]*)[ ]*\" \"([^ ]*)\" \"([^ ]*)\" ([^ ]*) ([^ ]*) \"([^ ]*)\" \"([^\\\"]*)\".*$')
    if (m.matches()) {
        println "${m[0][13]} ${m[0][17]}"
    }
}

これも障害調査でAWS ALBのログを分析する必要があった時に使いました6)Amazon Athena RegexSerDe を利用して ALB ログを探索する。ログの各行を正規表現でマッチさせて、そこのグループを指定して欲しい値を取り出しています。この例ではステータスコードとエンドポイントを取り出しています。
正直、一部を取り出すだけであれば sed でも十分ですが、他の処理を挟むこともあるためGroovyを使っています。
Groovyでは =~java.util.regex.Matcher が取り出せるため、正規表現のちょっとしたスクリプトを書くのにも便利です7)[Groovy]正規表現メモ8)Groovy学習4 正規表現を使用し、データをまとめて取り出す9)Groovyの奇妙な正規表現(Groovyの奇妙な演算子(3)改め)

おわりに

いかがでしたか?今後も不定期ですがGroovyの情報を発信していきたいと思いますので、よろしくお願い申し上げます。

p.s.

Markdownでは target="_blank" がうまく適用されなかったため、この記事内のリンクについてもGroovyスクリプトを利用して作成しております。

"""\
@eiryu,https://twitter.com/eiryu
Groovy,http://groovy-lang.org
[Groovy]GParsで並列処理(基本&コレクション編),https://qiita.com/saba1024/items/29bc32f3390facbaa5c5
""".eachLine {
    def columns = it.split(",")
    def title   = columns[0]
    def url     = columns[1]

    println """<a href="$url" title="$title" target="_blank">$title</a>"""
}