寝ても覚めてもこんぴうた

プログラム書いたり、ネットワーク設計したり、サーバ構築したり、車いじったり、ゲームしたり。そんなひとにわたしはなりたい。 投げ銭は kyash_id : chidakiyo マデ

GCP でアプリケーションのログや特定の値をいい感じに BigQuery にエクスポートしたい

f:id:chidakiyo:20201111192602j:plain

アプリケーションを実装していて、ログをかっちり決めたカラムに挿入したい、や、
難しい API を利用せずに BQ に特定のデータを挿入したいという場合ありますよね。

普通にやると、BigQuery の API を経由し、ストリーミングインサートするなどちょっとひと手間かけてデータを BQ に送っていると思います。

今回書く方法は、ある程度意図した構造(カラム)の状態で BQ に気軽にデータを送る方法になります。

サンプルとして、 Cloud Run での例になりますが、Cloud Logging に出力できる GCP のサービスならほぼ同じように利用できると思います。

概要

Cloud Run から stdout に対して JSON Payload 形式のログを出力します。
出力したログは Log Router を経由し、対象のログを絞り込みます。
絞り込んだログは BQ の Dataset へデータを流し込みます。

Go のコードを用意する

最低限のコードは以下のようなイメージになります

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/export", func(writer http.ResponseWriter, request *http.Request) {

        // BQに出力したい構造にしておく
        type ExportData struct {
            Type    string `json:"type"`
            Name    string `json:"name"`
            Num     int64  `json:"num"`
            Message string `json:"msg"`
        }

        // 値を詰め込む
        payload := &ExportData{
            Type:    "Type_A",
            Name:    "Export Data A Type",
            Num:     3,
            Message: "Export message.",
        }

        // stdoutにJSONとして吐き出す
        json.NewEncoder(os.Stdout).Encode(payload)

        fmt.Fprintf(writer, "ok")
    })
    port := "8080"
    if s := os.Getenv("PORT"); s != "" {
        port = s
    }
    if err := http.ListenAndServe(":"+port, mux); err != nil {
        log.Fatalf("http.ListenAndServe: %v", err)
    }
}

Go の構造体を作成し、適当なラベル(BQにカラムとして出力されるので適切なもの)をつけ、
stdout に対して JSON を吐き出します。

これを動かすと Cloud Logging にログが出力されます。

Logs Router で条件にマッチしたデータを BQ に送り込む Sink を作成する

Sink details, Sink destination は適当に設定しましょう。笑
Sink destination はもちろん BigQuery dataset を選択し、この設定を行う前に事前にBQ側でデータセットを作成しておく必要があります。

Choose logs to include in sink の設定だけ注意が必要で、
今回の例では

resource.type="cloud_run_revision"
jsonPayload.type = "Type_A"

f:id:chidakiyo:20201111192050p:plain

のように設定しましたが、JSON Payload の type ごとに BQ のデータセットを分けたい等の場合にはこのような設定にするのも良いでしょうし、Previewして自分が必要なログが対象になっていることを確認して利用してください。

完了したら Sink を作成します。
Sink の作成が完了しても、体感的に Sink の条件が反映され動き出すまでに 10分程度のラグがあるような気がします。
設定したのにうごかない!!と思っても、ちょっとのんびり待って、再度確認してみると良さそうです。

BQ に対してクエリする

ここまで設定がうまく行っていれば、アプリケーションにアクセスすることで BQ にログが保存されます。
先程構造体(JSON)として出力していますが、 BQ には構造体以外の Cloud Logging のログ部分も出力されていますが、
ここで構造体部分だけクエリしてみます。

以下のようなクエリを実行します

select jsonPayload.* from `{dataset}.run_googleapis_com_stdout` 

dataset名やテーブル名が異なることがあると思いますので、環境に合わせて指定します。
このクエリを実行すると以下のような結果が表示されます。

f:id:chidakiyo:20201111192115p:plain

4度ほど実行したので、4行の結果が出力されています。
今回の例では同一の値を出力してしまったので良いクエリの例が示せませんが、アプリケーションの任意の値を出力している場合には、一般的な SQL の知識で自由に利用できます。

まとめ

ちょっと雑な記事になってしまったけど、アプリケーション内から BQ に気軽に出力できると便利だと思う。
stdoutに出力する形だとローカル実行のテストなども行いやすいし。
ユースケースとしては、アプリの分析用の指標をポロポロ落としておいて、あとからざっと集計するなどアイディア次第で BQ と組み合わせて色々やれそうです。

ではでは。

たった 60 分で Go の静的解析が理解できる神コンテンツ

f:id:chidakiyo:20201008183032j:plain

タイトルの通りで 60 分ほどで静的解析が理解できるコンテンツを見つけたので共有。

静的解析ってなんじゃ?
ASTってなんじゃ?

って状態から、「なんとなく静的解析やれそう」になれたのでぜひ試してみてほしい。

静的解析をはじめよう - Gopherをさがせ!

IntelliJ で Eclipse の Outline 機能のような形でファイルのプロパティ一覧を表示したい

f:id:chidakiyo:20200908143237j:plain

Go を書いていると、 Go の文化としてファイルをあまり分割せずに、フラットにファイル内に書きづづけるということがお多くなるかと思いますが(自分は結構分割しちゃうけど)、その場合、どこにあのプロパティ/関数あったっけ?みたいになりますよね。

Eclipse だと Outline という機能があったけど、アレっぽい機能は IntelliJ にないの?と探したので記事にしました。

結論 : ある。

IntelliJ には Structure という機能で提供されています。
ドキュメントは このへん

Structure には2つの機能があって、

  • 構造ツールウィンドウ : ⌘ + 7
  • 構造ポップアップ : ⌘ + F12

があります

構造ポップアップは一時的に Structure のウィンドウをポップアップする機能で
私が求めていたのは「構造ツールウィンドウ」になるのでそちらで説明します。

構造ツールウィンドウを利用する

早速コマンドを実行して、ウィンドウを表示してみます。

⌘ + 7

を実行すると、IntelliJ 上のどこかに Structure のウィンドウが表示されます。
その他の IntelliJ のウィンドウと同様に適当な箇所にスナップできますので、私は右側にこのように表示するようにしてみました。

f:id:chidakiyo:20200908143036p:plain

Structure 上の関数名やプロパティ名をクリックすると実装箇所にエディタ側が飛ぶのでサクサク目的の箇所に移動することができ便利ですね。

ではでは。

Spanner で NOT Nullのカラムを追加したい

f:id:chidakiyo:20200908094813j:plain

Spanner は RDB のようにかっちりとスキーマを定義する DB なので、
Datastore などのように、プロパティをふわっと追加してデータ投入などできません。

RDB と同じように DDL を利用してカラム追加して利用しますが、 NOT NULL のカラムの追加ができないという特性があります。

今回は以下のようなスキーマで考えます。

CREATE TABLE Member (
    ID STRING(256) NOT NULL,
    Name STRING(256) NOT NULL,
) PRIMARY KEY (ID);

NOT NULL カラムが追加できないことを確認する

以下のようなクエリを実行し、 NOT NULL の制約のあるカラムを追加しようとしてみます

ALTER TABLE Member ADD COLUMN Nickname STRING(256) NOT NULL;

→ 実行すると、 NOT NULL なカラムは追加できないよ。 という感じに怒られてしまいます。

NULL 許可したカラムを追加してみる

NOT NULL なカラムは追加できないが、 Nullable なカラムは追加できるはずなので追加してみます。

ALTER TABLE Member ADD COLUMN Nickname STRING(256);

→ 問題なく追加できますね。

Nickname カラムをなんとか NOT NULL にしてみる

例えば、 NOT NULL にカラムを変更しようとしても、 Nullable なカラムを追加した状態では NULL が入っているので、変更は失敗するはず。
なので、 NULL な値をなくして、 NOT NULL なカラムに変更するというのを試してみる。

ためしに、 Nullable なカラムに NULL が入った状態で NOT NULL に 変更してみる。

ALTER TABLE Member ALTER COLUMN Nickname STRING(256) NOT NULL;

→ 予想通り Adding a NOT NULL constraint on a column Member.Nickname is not allowed because it has a NULL value at key: [xxx] といった形で NULL な値があるので NOT NULL の制約はつけられないと怒られた

NULL 値を Empty String に置き換えてみる(なにか特定のデフォルト値でもいいと思います)

UPDATE Member SET Nickname = '' WHERE Nickname IS NULL;

NULL を置き換えたら NOT NULL 制約をつけてみる

ALTER TABLE Member ALTER COLUMN Nickname STRING(256) NOT NULL;

→ 結果うまく行きます。

最終的にこのようなテーブル構成になります

CREATE TABLE Member (
    ID STRING(256) NOT NULL,
    Name STRING(256) NOT NULL,
    Nickname STRING(256) NOT NULL,
) PRIMARY KEY (ID)

NOT NULL なカラムを追加するための流れの要約

  1. NULL 許可する形でカラムを追加する
  2. NOT NULL にしたいカラムの場合には NULL ではない値にカラムを書き換える (注意点あり *1)
  3. NOT NULL 制約を満たせる状態になったら ALTER 文で改めてスキーマ変更する。

の3つの手順を経る必要がある。(default的なものがない)

注意(この点注意しましょう!!)

*1) カラム書き換えはUPDATEステートメントなどを利用するが、Spannerの制約で 20k 以上の変更が1クエリで行えないので注意。(また、20k はレコード数でもない点も注意すること)

まとめ

機能的には NOT NULL のカラムを追加することはできるようなのですが、最後の 注意 の点だけ気をつけましょう
運用が始まって大量のレコードが追加されているテーブルの場合、Spanner の一度に更新できる制限(20k)のために非常に大変なことになります
場合によってはパーティション DML を利用したりするのかな?(参考のリンク先参照)

参考

cloud.google.com

cloud.google.com

Spanner のスキーマ管理をする wrench を試してみた

f:id:chidakiyo:20200903171222j:plain

先日 Spanner のスキーマ管理ツール hammer を利用した ブログ を書きましたが、
Cloud Spanner Echosystem から wrench というツールが提供されていることに気づきました。

hammer 最近更新されてないのかな(?)、と思いながら試していたところもあるので、今回はこちらの wrench を試してみます。

手順そのものは公式の ドキュメント にほぼ沿ったものになります

wrench のインストール

go get -u github.com/cloudspannerecosystem/wrench
wrench

を実行することで色々表示が出れば成功。
(何故か hammer もそうでしたが、 wrench も -v オプションでバージョンを見ようとすると unknown と表示されます・・・)

環境変数に接続情報をもたせる

以下のような環境変数を事前に登録します

export SPANNER_PROJECT_ID=your-project-id
export SPANNER_INSTANCE_ID=your-instance-id
export SPANNER_DATABASE_ID=your-database-id

スキーマファイルを用意する

最初にまずスキーマのファイルを用意します。
今回は _ddl というディレクトリを利用しようと思います。

作成するファイルは以下のようになります

cat ./_ddl/schema.sql
CREATE TABLE users (
  user_id STRING(36) NOT NULL,
) PRIMARY KEY(user_id);

スキーマを適用する

以下のコマンドで DB にスキーマを適用します。
事前にすでに DB が存在している場合にはコマンドがエラーになるので注意してください。

wrench create --directory ./_ddl

コマンド実行が成功すると、 database と table が作成されます。

マイグレーションファイルを作成する

コマンドを実行してマイグレーションファイルのテンプレートを作成します。

wrench migrate create --directory ./_ddl

_ddl/migrations/000001.sql is created と表示され、 {連番}.sql という空のファイルが作成されます。

カラムの変更を migrate ファイルに記述する

試しに新しいカラムを追加してみましょう。
先程生成された {連番}.sql ファイルに対して以下のように記述します。

ALTER TABLE users ADD COLUMN age INT64
ALTER TABLE users ADD COLUMN name STRING(MAX)
ALTER TABLE users ALTER COLUMN name STRING(MAX) NOT NULL
CREATE INDEX idx_users_name ON users(name)

マイグレーションの実行

以下のコマンドでマイグレーションを実行します

wrench migrate up --directory ./_ddl

1/up などと表示されれば成功です。

schema.sql に DB 内のスキーマを反映する

この段階ではローカルの schema.sql はマイグレートした内容を反映していないので、wrench コマンドを使って最新化してみます。

wrench load --directory ./_ddl

コマンドが成功すると、 schema.sql ファイルの中身は以下のように更新されてるはずです。

CREATE TABLE SchemaMigrations (
  Version INT64 NOT NULL,
  Dirty BOOL NOT NULL,
) PRIMARY KEY(Version);

CREATE TABLE users (
  user_id STRING(36) NOT NULL,
  age INT64,
  name STRING(MAX) NOT NULL,
) PRIMARY KEY(user_id);

CREATE INDEX idx_users_name ON users(name);

注意点としては、スキーマ管理用のテーブル SchemaMigrations も併せて生成されています。
Spanner のコンソールから確認するとわかりますが、最後に適用したマイグレーションファイルの番号がそのテーブルに追加されていることがわかります。

まとめ

hammer は ALTER 文の生成までツールでやってくれていましたが、 wrench はあくまでも DDL はユーザが記述し、 wrench は適用した DDL のバージョンの管理まで、という感じで似たようなツールではありますが、やれることが割と違います。

wrench のほうが暗黙でやっていることが少ないので、 本番運用に使うツールとしては wrench が良さそうで、
開発時に、テーブルスキーマを軸に修正しつつコツコツ作業したい場合には hammer が便利なんじゃないかと思ったりしました。

ではでは。

Spanner の DDL (スキーマ)管理を行う hammer を試してみた

f:id:chidakiyo:20200902100259j:plain

Datastore を利用していたときにはスキーマがあってなかったようなものなので、基本的にはエンティティの定義 ≒ スキーマみたいなところがあったが、Spannerは正統派RDB的な振る舞いをするので、 DDL によってスキーマを定義します。

スキーマの設計自体を DDL で行った場合、運用が始まったテーブルに対してはスキーマ自体ではなく、差分のクエリを発行する必要があります。(ALTER TABLE)

RDB を使ってきた人としては「まぁそうだよね」という感じの話ではあるが、毎回差分を把握して ALTER 文を作成するのも面倒なので、そのあたり「いいかんじ」にやってくれるという hammer を試してみます。

hammer をインストールする。

インストールにはバイナリでインストールする方法と、 go get を利用する方法がありますが、私の環境は go がすでにインストールされてるため、 go get コマンドを利用してインストールします。

go get -u github.com/daichirata/hammer

インストールが終わるまでちょっと待ちます。

注意 : Spannerライブラリ1.9 で若干構成が変わっているため go get でインストールできない場合があります。その場合はバイナリからのインストールをおすすめします。(2020/09/02現在)

インストールを確認

以下のコマンドで正しくインストールされたか確認してみましょう

hammer

色々それっぽい情報(?)が表示されれば OK です。(-v オプション実装されてない??)

2つのファイルから差分を出してみる

ドキュメントの例の通り、以下のような2つのファイルを用意してみます。

users.sql

CREATE TABLE users (
  user_id STRING(36) NOT NULL,
) PRIMARY KEY(user_id);

users2.sql

CREATE TABLE users (
  user_id STRING(36) NOT NULL,
  age INT64,
  name STRING(MAX) NOT NULL,
) PRIMARY KEY(user_id);
CREATE INDEX idx_users_name ON users (name);

これらを用意した上で以下のコマンドを実行してみます

hammer diff users.sql users2.sql

結果は以下のように表示されました。

ALTER TABLE users ADD COLUMN age INT64
ALTER TABLE users ADD COLUMN name STRING(MAX)
UPDATE users SET name = '' WHERE name IS NULL
ALTER TABLE users ALTER COLUMN name STRING(MAX) NOT NULL
CREATE INDEX idx_users_name ON users(name)

良さそうな感じですが、befor/after の2つのファイルを用意するというパターンは Git で DDL を管理している場合にはあまり発生しないパターンだと思うので、 Spanner の DB と直接比較するパターンを試してみます。

DB とファイルの差分を出してみる。

先程の users.sql を Spanner 側に適用した状態で、 users2.sql と比較してみます。

hammer diff spanner://projects/{PROJECT_ID}/instances/{INSTANCE_ID/databases/{DB_NAME} users2.sql

コマンドを実行すると以下のような結果が表示されました

ALTER TABLE users ADD COLUMN age INT64
ALTER TABLE users ADD COLUMN name STRING(MAX)
UPDATE users SET name = '' WHERE name IS NULL
ALTER TABLE users ALTER COLUMN name STRING(MAX) NOT NULL
CREATE INDEX idx_users_name ON users(name)

先ほどと同じ感じになりますね。
DBに適用したスキーマをベースに、新しく追加した DDL との差分を得るという運用の場合にはこの感じで利用できそうです。

おまけ : スキーマファイルを Spanner DB に適用する

ファイルを DB に適用する場合には apply コマンドを利用します。

hammer apply spanner://projects/{PROJECT_ID}/instances/{INSTANCE_ID/databases/{DB_NAME} users2.sql

ただ、裏側で何をやっているかあまり把握できていないので、プロダクション環境では利用しない感じでしょうか。

ちょっと不安な点

diff の定義的に、(コマンドでファイル連結するてもあるが) DDL ファイルは1ファイルで管理するイメージになるでしょうか。
テーブルなどが増えてきた際にファイル分割したくなったりするパターンもあるかな?
と思うところもあったので、使ってみて使用感とかまたアウトプットできればと思います。

ではでは。

参考

Spanner で 羃等スキーマ管理をするツール hammer の話 - Qiita

Google Cloud Japan イベントアーカイブ(オンラインイベントのアーカイブ一覧ページ)ができたっぽい

f:id:chidakiyo:20200901134824p:plain

最近は GCP 関連のイベントもほとんどがオンラインになりましたね。

興味ありそうな内容のイベントがあっても気づけなかったり、知っていても調べるのが大変、というのがあったと思いますが、公式にイベントアーカイブページが公開されていました。

Google Cloud Japan '20 イベント アーカイブ - ホーム

とても便利ですね。

私も漏らしているものなど、振り返っていろいろ見てみようと思います。

ではでは。