Kotlin MultiplatformサポートのGoogle Analyticsライブラリ「Measurer」をつくった

年始から個人的に大流行のKotlin Multiplatform。開発が楽しい。BFF Server触るエンジニアとネイティブエンジニアの距離感が非常に近くなる。「いや、チームとして当たり前でしょ」みたいなツッコミはあるだろうけど、新しい技術を軸にエンジニア達が「あーだこーだ」言ってる楽しい時間がKotlin Multiplatformでは職域が違うエンジニアが議論することで更に熱が加速する現象を目の当たりにしてます。

モチベーション

私はBFF Serverのほうを主に開発していましたが、プラットフォーム横断で共通するロジックをcommonモジュールにつくった辺りからKotlin Multiplatformサポートのライブラリの開発欲が湧いてきました。

当時ですと既存していた2つのライブラリに着目しました。

github.com

github.com

Kissmeはセキュアストレージの扱いをマルチプラットフォーム化できて、KonformはValidationのユーティリティなライブラリです。また社内からも自作ライブラリが飛び出していたりと活発な流れを感じていました。

どのライブラリも「プラットフォーム横断で共通するロジック」であればKotlin Multiplatformにするコンセプトがあり、私も何か作りたいなーと感じていました。同僚の@AAkiraからGoogle Analyticsのログ送信部分をライブラリするのはどうだろうか、という話をしたのをきっかけにGoogle Analyticsライブラリを作るモチベーションが高まりました。

Measurer

そして開発をしたのがMeasurerです。

github.com

GoogleはMeasurement ProtocolというHTTPリクエストのプロトコルを設計しています。このプロトコルをつかえばHTTP/1.1でGoogle Analyticsへログが送信できます。ライブラリはMeasurement Protocolに準拠して開発を行いました。

2019/2時点で公開されているパラメータとヒットタイプの全てをサポートしています。また1件ずつ送信するのではなくバッチリクエストにも対応していて20件までは一度に送信できます。

今のところJVMとAndroidをサポートしていてiOSとJSは順次対応していく予定です。

使い方

Readmeに記載しているので簡単に使い方を紹介します。

Measurerをプロジェクトに依存させたらcommon moduleに送信部分を実装します。

object GoogleAnalytics {

    private val httpClient = SampleHttpClient(SampleHttpClientConfig.httpClient, SampleNapierLogger())

    suspend fun pageTracking(ua: String) {
        val mp = MeasurementProtocol
            .Builder(
                trackingId = ua,
                httpClient = httpClient
            ).build()

        mp.also { ga ->
            ga.pageView("mydemo.com", "/home").apply {
                clientId = "555"
                documentTitle = "homgepage"

                contentGroup = ContentGroup(index = 1).apply {
                    value = "news/sports"
                }
            }

            ga.pageView("mydemo.com", "/home").apply {
                clientId = "555"
                documentTitle = "homgepage"

                contentGroup = ContentGroup(index = 2).apply {
                    value = "news/finance"
                }
            }
        }.send()
    }
}

上記のGoogleAnalyticsをJVMとAndroidのプラットフォームから呼び出すだけです。

GoogleAnalytics.pageTracking(trackingId)

Google Analyticsの送信部分がワンソースで管理され各プラットフォームでマルチユースできています。

ライブラリ標準でHTTPクライアントはKtor、ログ出力はNapierを使っています。この部分はプロジェクトで使いたいライブラリに置き換えられるようにしています。

「ライブラリに置き換えられるようにしている」としましたが、HTTPクライアントやログのライブラリは2019/2時点ではKtorとNapierの選択肢しかないです。Kotlin Multiplatform界隈のライブラリはそこまで出てきてない状況をお分かりいただけると思います。

まとめ

  • Kotlin MultiplatformサポートのGoogle AnalyticsライブラリであるMeasurerの紹介でした。
  • まだiOS、JSのサポートが残っているので引き続きやっていきなモチベーションです。
  • Kotlin Multiplatformは楽しい。Kotlin言語を軸にしたAndroid, iOSの両方ができるエンジニアは楽しめる領域です。
  • 私はiOS, JS頑張らないとなーと伸ばすべき領域を感じました。足りないスキルセットを見つめ直す機会が体験できるのもKotlin Multiplatformの良きポイントですね。

Prowの真骨頂であるTideでPRの自動マージを導入する。

前回のエントリ「ProwではじめるChatOps on GitHub。」からProwを完全理解した!と言ってはいけない。Prowの真骨頂はTideにあると思う。

f:id:n_soushi:20190109113455p:plain

Enable kustomize in kubectl by Liujingfang1 · Pull Request #70875 · kubernetes/kubernetes · GitHub

上記のProwが有効になったPRのフローの最後を見てほしい。BotアカウントがPRをマージしているのだ。この仕組みはProwのTideが実現してくれる。

github.com

Tideは一定の条件を満たした上でPRをマージしてくれる。マージを行うアカウントはProwに設定したGitHubのアクセストークンになるのでBotアカウントになる。一定の条件は下記のようなyamlでセットアップを行う。

tide:
  merge_method:
    kubeflow/community: squash

  target_url: https://prow.k8s.io/tide.html

  queries:
  - repos:
    - kubeflow/community
    - kubeflow/examples
    labels:
    - lgtm
    - approved
    missingLabels:
    - do-not-merge
    - do-not-merge/hold
    - do-not-merge/work-in-progress
    - needs-ok-to-test
    - needs-rebase

上記のqueriesに含まれるreposやlabelsの条件を満たせばTideがPRをマージしてくれる。

このエントリでは

このエントリでは、Tideのセットアップの方法とPRフローに必要そうなProwプラグインのセットアップ方法をまとめていく。

Triggerプラグインを有効にする

Triggerプラグインは /ok-to-test, /test xxxx, /retestなどのキーワード入力からProwJobsを実行するプラグインである。(Prow Plugin Catalogのtriggerを参照)

Triggerプラグインのセットアップ手順をまとめていく。次のようなconfig.yamlを用意する。

presubmits:
  soushin/bazel-multiprojects:
  - name: unit-test
    always_run: false
    skip_report: false
    spec:
      containers:
      - image: alpine
        command: ["/bin/printenv"]

上記のコンフィグレーションにより /test unit-testというキーワードが有効になった。Prowはキーワードとレポジトリ(soushin/bazel-multiprojects)がマッチすればPodを起動してalpineイメージから /bin/printenvのコマンドを実行する。e2eテストが実行できるコンテナイメージを用意すればGitHubのコメントからe2eテストが実行できるようになる。ここらへんはプロジェクトに応じてしっかりとした準備と検討が必要なところだ。

always_runやskip_reportはプロジェクトに応じてセットアップする。詳細はjobs.mdにまとまっているので参照してほしい。

上記のconfig.yamlを用意したらConfigMapに反映する。

(test-infra) $ cat config.yaml
presubmits:
  soushin/bazel-multiprojects:
  - name: unit-tests
    always_run: false
    skip_report: false
    spec:
      containers:
      - image: alpine
        command: ["/bin/printenv"]
(test-infra) $ kubectl create configmap config \
  --from-file=config.yaml=config.yaml --dry-run -o yaml \
  | kubectl replace configmap config -f -

またplugin.yamlにはTriggerプラグインを有効にした上でConfigMapに反映する。

(test-infra) $ cat plugins.yaml
plugins:
  soushin/bazel-multiprojects:
  - size
  - trigger
(test-infra) $ kubectl create configmap plugins \
  --from-file=plugins.yaml=plugins.yaml --dry-run -o yaml  \
  | kubectl replace configmap plugins -f -

これでProwのhookポッドにTriggerプラグインとTrigger対象のProwJobs(unit-tests)が有効になった。

Triggerを実行する

任意のPRを作成して /test unit-testsを入力した後にCheckにProwJobsが加わっている。

f:id:n_soushi:20190109124032p:plain

もしProwJobs(unit-tests)がエラーになればBotアカウントがお知らせしてくれる。

f:id:n_soushi:20190109124440p:plain

エラーになればコードを修正して /retestを入力して全てのテストを実行する。Prowは登録されているProwJobsを実行して結果をGitHubに返す。

Triggerの流れを整理

Triggerプラグインを有効にしたProwは次のような流れになる。

  • ReviewerはPRコードを確認してOKなら /ok-to-testを入力する。
  • Prowは ok-to-testのラベルを付与する。
  • Comitter(またはReviewer)は /test xxxxを入力してProwJobsを実行する。
  • Prowはレポジトリに有効になったProwJobsを実行して結果を報告する。
  • エラーの場合は /retestを促す。
  • ReviewerはChecksを確認して必要なProwJobsがすべてSuccessedになっているか確認する。

上記のChatOpsのフローを交えてPRマージまで進める。

LGTM, Holdプラグインを有効にする

Tideを有効にする前にLGTM, Holdプラグインを確認していきたい。(Prow Plugin Catalogのlgtm, holdを参照)

このプラグインは/lgtm,/holdのキーワードを入力するとそれぞれlgtm,do-not-merge/holdのラベルを付与してくれるプラグインである。これを有効にしてTideの設定値であるlabelと組み合わせる。

次のようにlgtmholdを有効にしてConfigMapに反映する。

(test-infra) $ cat plugins.yaml
plugins:
  soushin/bazel-multiprojects:
  - size
  - trigger
  - lgtm
  - hold
(test-infra) $ kubectl create configmap plugins \
  --from-file=plugins.yaml=plugins.yaml --dry-run -o yaml  \
  | kubectl replace configmap plugins -f -

/hold/hold canceldo-not-merge/holdラベルの付与と取外しを行い/lgtmlgtmラベルを付与している。

f:id:n_soushi:20190109130622p:plain

※ 1人でエントリをまとめていたので/lgtm/hold cancelをBotアカウントが行ってしまっているが本来はコントリビューターが行うことになる。

Tideを有効にする

PRマージを行うまでに必要なテスト(Trigger)とラベル付与(LGTM, Hold)を有効にするプラグインをまとめきた。これでTideを有効にしたPRフローの準備ができた。

Tideはconfig.yamlに次のようなコンフィグレーションを追加して有効にした。

tide:
  queries:
  - repos:
    - soushin/bazel-multiprojects
    labels:
    - lgtm
    missingLabels:
    - do-not-merge/hold

PRマージの対象のレポジトリと必要なラベルと必要ないラベルの設定が有効になっている。

これまでと同様にconfig.yamlをConfigMapに反映する。

(test-infra) $ kubectl create configmap config \
  --from-file=config.yaml=config.yaml --dry-run -o yaml \
  | kubectl replace configmap config -f -

Tideが有効になったPRフローを確認する

Tideが有効になったPRにはChecksにtideが追加されている。

f:id:n_soushi:20190109131832p:plain

lgtmのラベルが必要なのでラベルを付与するとProwが自動でPRをマージしくれる。

f:id:n_soushi:20190109132118p:plain

まとめ

  • ProwのTideを有効にする方法をまとめた。
  • またTideをPRフローに合わせるために必要なProwプラグインをまとめた。
  • TideがあればマージまでのPRフローが明確になる。オープンなレポジトリを運営する際のコントリビュートのルールやプロジェクトチームのPRルールにProwを導入すればコミッターは迷わずPRフローを進めることができる。
  • ProwプラグインにはassignwipなどPRフローに必要なものがあるのでチームにフィットしたプラグイン選びをしていきたい。

ProwではじめるChatOps on GitHub。

Kubernetesのtest-infraレポジトリにあるProwを試す。ProwはKubernates環境を基盤としたCIとCDのシステムである。

モチベーション

Kubernatesを追っている人であれば次のようなコメントのやり取りをGitHub上で見かけたことがあるだろう。

f:id:n_soushi:20190108105515p:plain

Enable kustomize in kubectl by Liujingfang1 · Pull Request #70875 · kubernetes/kubernetes · GitHub

/hold/testなどのキーワードをコメント欄に入力してオペレーションを実行している。/hold cancelでラベルを取り除いたり/testでテストを実行している。入力したキーワードはWebhookで稼働しているProwのエンドポイントに送信されてGitHubイベントと合わせてProwのプラグインが実行される仕組みである。

このProwの導入手順をまとめていきプロジェクトにフィットするか検討したいというのがエントリのモチベーションである。ProwはKubernetesをはじめIstio、Knative、Prometheusなどのオーガナイゼーションにも利用されている。

Prowをデプロイする

Prowが含まれるレポジトリはkubernetes/test-infraである。

github.com

デプロイ手順はgetting_started_deploy.mdを参考に進めている。

test-infra/getting_started_deploy.md at master · kubernetes/test-infra · GitHub

gcpを利用している人であればTackleというユーティリティが用意されているので対話式にProwをセットアップできるが私はローカル(Docker For Desktop)でKubernatesを起動させているためManual deploymentを参考にすすめた。

またClusterとNamespaceはdocker-for-desktopdefaultとしている。実際の運用ではプロジェクトに合わせた選択が求められる。

事前に準備するものを一覧にする。

  • Prowを試すレポジトリ
  • Botアカウント(GitHubのBot専用アカウント)
  • BotアカウントのAccessToken
  • ご自身のアカウントのAccessToken

Kubernatesはk8s-ci-robotというBotアカウントを使用している。

k8s-ci-robot (Kubernetes Prow Robot) · GitHub

kubernetes/test-infra をチェックアウト

$ git clone git@github.com:kubernetes/test-infra.git
$ cd test-infra

以降の手順はすべてtest-infraのディレクトリで行う。

GitHubとProwを連携するための準備

GitHubとProwを連携するための準備としてWebhookで送信するSecretキーを生成しSecretリソースに追加する。

(test-infra) $ openssl rand -hex 20 > hook_secret
(test-infra) $ kubectl create secret generic hmac-token --from-file=hmac=hook_secret

BotアカウントのGitHubアクセストークンをSecretリソースに追加する。

# BotアカウントのGitHubアクセストークンを`bot_oauth_secret`に保存している
(test-infra) $ kubectl create secret generic oauth-token --from-file=oauth=bog_oauth_secret

Prowのコンポーネントをデプロイする

(test-infra) $ kubectl apply -f prow/cluster/starter.yaml

Deploymentは次のようになっている。

(test-infra) $ kubectl get deployments 
NAME         DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deck         2         2         2            0           3m
hook         2         2         2            2           3m
horologium   1         1         1            1           3m
plank        1         1         1            1           3m
sinker       1         1         1            1           3m
tide         1         1         1            1           3m

deckがエラーになっているがOAuth Appを利用するケースのため割愛する。

test-infra/pr_status_setup.md at master · kubernetes/test-infra · GitHub

WebhookをGitHubに追加する

私の環境はローカルのためIngressにIPアドレスが割り振られないがGitHubのWebhookから叩かれるエンドポイントのhookPodはNodePortで開かれているのでngrockをつかってプロキシしている。

NodePortを確認する。

(test-infra) $ kubectl describe service hook
Name:                     hook
Namespace:                default
Labels:                   <none>
Annotations:              kubectl.kubernetes.io/last-applied-configuration:
                            {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"hook","namespace":"default"},"spec":{"ports":[{"port":8888}],"sel...
Selector:                 app=hook
Type:                     NodePort
IP:                       10.98.120.195
LoadBalancer Ingress:     localhost
Port:                     <unset>  8888/TCP
TargetPort:               8888/TCP
NodePort:                 <unset>  31797/TCP
Endpoints:                10.1.7.24:8888,10.1.7.25:8888
Session Affinity:         None
External Traffic Policy:  Cluster
Events:                   <none>

確認したNodePort 31797をngrockでプロキシする。

(test-infra) $ ngrok http 31797

Forwardingに表示されているhttps://xxxxxx.ap.ngrok.ioのURLをWebhookに利用する。

Webhookを追加するユーティリティをつかってみる

ProwにはWebhookを追加するユーティリティが用意されている。ユーティリティを使わなければ手動でGitHubからWebhookを追加すれば良い。

(test-infra) $ bazel run //experiment/add-hook -- \
  --hmac-path=/path/to/hook_secret \
  --github-token-path=/path/to/own_oauth_secret \
  --hook-url https://xxxxxx.ap.ngrok.io/hook \
  --repo soushin/bazel-multiprojects \
  --confirm=true
  • hmac-pathには生成したhook_secretを指定(注:絶対パス)
  • github-token-pathにはご自身のGitHubアクセストークンを保存したown_oauth_secretを指定(注:絶対パス)
  • hook-urlにはngrockでプロキシしたhttps://xxxxxx.ap.ngrok.io/hookのエンドポイントを追加して指定
  • repoにはProwを試すレポジトリを指定

実行後にINFOが次のように出力されていれば完了。

INFO[0000] ListRepoHooks(soushin, bazel-multiprojects)   client=github
INFO[0000] CreateRepoHook(soushin, bazel-multiprojects)  client=github

GitHubのSettings -> WebhooksのページにWebhookが追加されていてグリーンのチェックマークが点いていれば正常に完了。

これでKubernatesにデプロイしたProwのコンポーネントとGitHubがWebhookで連携できた。次にプラグインを追加してProwのChatOpsを体験していく。

Prowプラグインを追加する

プラグインを追加する前にProwのhookPodのログにno pluginsのエラーが確認できる。

hook-64448bb489-42v8b hook {"component":"hook","level":"warning","msg":"no plugins specified-- check syntax?","time":"2019-01-08T03:07:15Z"}
hook-64448bb489-42v8b hook {"component":"hook","level":"warning","msg":"no plugins specified-- check syntax?","time":"2019-01-08T03:08:15Z"}

つまり何かしらプラグインを追加して初めてProwのChatOpsが体験できるわけだ。

Prowのプラグインはこちらにまとまっている。

Prow Plugin Catalog

Sizeプラグインを試す

SizeプラグインはPushしたコード量に応じてBotアカウントがsize/SSize/XLなどのラベルをプルリクエストに付与してくれるプラグインである。

plugins.yamlを次のように作成する。

(test-infra) $ echo 'plugins:
  soushin/bazel-multiprojects:
  - size' > ./plugins.yaml

soushinbazel-multiprojectsレポジトリにsizeプラグインを有効にしている。soushinのみを指定してオーガナイゼーション全体にプラグインを有効にすることも可能である。

plugin.yamlをConfigMapに追加する。

(test-infra) $ kubectl create configmap plugins \
  --from-file=plugins.yaml=plugins.yaml --dry-run -o yaml  \
  | kubectl replace configmap plugins -f -

追加したConfigMapはhookPodにマウントされてプラグインが有効になる。

適当なプルリクエストを追加するとBotアカウントがSizeラベルを付与してくれる。

f:id:n_soushi:20190108122855p:plain

まとめ

  • Prowの導入手順をまとめた。
  • Kubernatesは1日にテストを1,000回以上実行するそうでスケーラビリティが求められる。ProwはKubernates環境で稼働するためニーズとマッチした仕組みである。
  • Jenkins XはKnativeとProwを基盤に実現したサーバレスJenkinsで、Knativeというサーバレスの仕組みにProwプラグインの拡張性が組み合わさった感じなのかな、という印象を持った。
  • 今回は既存のプラグインを試したがtriggerというプラグインはtestに関連していそうで非常に気になる。
  • プロジェクトにマッチするか引き続きProwを触っていきたい。