Cloud loggingのシンクを使って、BQにJsonログを流し込む際のトラップと回避方法
TL;DR
Cloud loggingのシンクを使うと、Cloud loggingで収集したデータをBQ・GCS・pubsubに転送できる。
BQへと転送される際に、ログがJsonの場合、自動でBQの構造型としてパースされる。そのため、いろいろJsonに情報を詰め込んでいると、テーブルのスキーマが壊れる。
上記を回避するには、LogAnalytics機能つきのバケットへシンクしてからBQに転送するのがおすすめ
Cloud loggingのシンクとは
Cloud Logging に入ってきたログは、シンクによって宛先であるログバケットや BQに振り分けられ、転送される。
実際のロギング
Jsonログを標準出力に流すロガーの設定
def getLogger():
handler = StructuredLogHandler(stream=sys.stdout)
logger = logging.getLogger("mylogger")
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False
return logger
logger = getLogger()
APIのルートにアクセスがあった場合、Jsonログを出力するようにする
@app.get("/")
async def root():
payload = {
"val1":random.randrange(100),
"val2":random.randrange(100),
"val3":random.randrange(100)}
logger.info(payload)
return {"message": "Hello World"}
実際にアプリケーションを動かしアクセスすると、cloud loggingの画面で、 val1, val2, val3に関するログが確認できる
cloud loggingのログルーターから、シンクを作成し、BQにデータを流し込むようにする
次走でJsonログをパースしてBQのテーブルに取り込んでくれる
注意点その1
シンクの転送先は、テーブルではなく、データセットの指定しかできない。
そのため、
- アプリケーションが複数あって、ログを分けたい
- ログの種類ごとにわけたい
という場合、
- BQに取り込む際に、データセットを分ける
- 一旦全てのログデータをまとめて格納して、ETLで別テーブルに分離する
という方法しかない。データセットが乱立するのはよくないので、実質後者しか選択肢がない。
また、生成されるテーブルの命名の変更が(おそらく)不可能
データセット名は指定できるが、テーブルはできないため、下記のような感じになる
注意点その2
JsonPayloadに取り込まれる際に、勝手に型が決定する。型の指定はすることができない。
実際には、intしか入っていないが、float扱いになっている
注意点その3
Jsonログは、BQに転送される際に勝手にパースしてくれるので、便利ではある。
しかし、JsonPayloadに他のキーが入った場合、BQ側で自動でスキーマが変更される。
そのため、Jsonログにいろいろ詰めて、いろんな種類のログをはくと、テーブルのカラムが大量に増えてしまう & nullが大量に発生する。
下記は、val5のJsonキーを後から追加したため、カラムが増え、nullが大量に増えている図。
これが大量に発生すると、そのテーブルに対してクエリをはくのが辛くなる。
(一応、Jsonを文字列に変換して、TextPayloadとしてログ出力すれば、上記は回避できる。しかし、間違えて変換を忘れてロギングを行なってしまった場合、BQのテーブルのスキーマが壊れて修復が大変になるので、あまり良くなさそう)
回避策
cloud loggingのDefaultログバケット(もしくは新規に作成したログバケット)を、LogAnalytics機能付きにアップグレートする。そのログバケットをBQに転送するようにチェックボックスにチェックをいれる。
すると、ログバケットのデータがBQに転送する際に、JsonPayloadの部分は、BQのJson型として読み込んでくれる。
そのため、自由にJsonPayloadの中にデータを入れてアプリケーション側が送信しても、BQのテーブルが壊れない。
Json型なので取り出しも簡単
SELECT
json_payload.val1,
json_payload.val2,
json_payload.val3,
json_payload.val4,
json_payload.val5,
FROM
`hogehoge.logs._Default`
WHERE json_payload is not null
AND json_payload.val1 is not null
必要なデータは、ETL処理などで、別テーブルに書くのが良さそうな気がした。スキャンする量にもよるが、Viewでも問題ないと思う。
欠点としては、2023/01月にGAになったばかりなので、2023/02月時点ではterraformでの設定ができない 点。そのうちできるようになると思う。
Discussion