🦔

Cloud loggingのシンクを使って、BQにJsonログを流し込む際のトラップと回避方法

2023/02/07に公開

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