GCP上でHaskellプログラムからStackDriver Loggingにログを出力する


Google Cloud PlatformのVMインスタンス上でHaskellプログラムを動かし、StackDriver Loggingにログを出力する方法を調べた。

コード全体

サンプルとして、https://github.com/steshaw/google-logging-example を見つけたが、GKE(コンテナ)上で動作するものだった。
今回は、GCPのVMインスタンス上で実行ファイルを動かしたかったので、forkしてから、少しいじって、https://github.com/masatoko/google-logging-example/tree/gce-instance を作った。コード全体はこの'Main.hs'

利用するライブラリgogol

GoogleのAPIをHaskellでいじれるライブラリgogolを使う。
https://github.com/brendanhay/gogol

https://hackage.haskell.org/package/gogol
https://hackage.haskell.org/package/gogol-logging

コードの要点

要点は、(1)MonitoredResourceDescriptorの設定、(2)ラベルの設定、(3)LogNameのフォーマットだ。
下のコードと照らし合わせながら、各要点を確認してほしい。

(1) MonitoredResourceDescriptor

https://cloud.google.com/monitoring/api/resources の表を参照して、目的のTypeとLabelを見つける。今回は、VM上のプログラムなので、Typeは'gce_instance'で、対応するラベルは、'project_id', 'instance_id', 'zone'だ。(実はこの表を参照すればいいのかは自信がないのだけれど、成功したから合っていると思う。間違っていたらご指摘ください。)

(2) ラベルの設定

任意のラベルを指定できる。コード中では、[("key", "value")]となっているところ。

(3) LogNameのフォーマット

projects/{プロジェクトID}/logs/{任意のログID} としないとエラーとなるので注意。

コード(解説したいところだけ)

logMsg :: Text -> Text -> IO (Rs EntriesWrite)
logMsg logId msg = do

  -- ~~~~~
  -- 省略
  -- ~~~~~

  let
    entry = logEntry & leTextPayload ?~ msg
                                     ?~ Info -- Network.Google.Logging - LogEntrySeverity -- https://hackage.haskell.org/package/gogol-logging/docs/Network-Google-Logging.html#t:LogEntrySeverity
    entries = [entry]
    logName = "projects/" <> projectId <> "/logs/" <> logId -- (3) LogNameはこのフォーマットである必要がある!
    resourceLabels = monitoredResourceLabels $ HM.fromList -- (1) https://cloud.google.com/monitoring/api/resources の'Description Labels'
      [ ("project_id", projectId)
      , ("instance_id", instanceId)
      , ("zone", zone)
      ]
    resource = monitoredResource -- (1) これは必須
      & mrType ?~ "gce_instance" -- https://cloud.google.com/monitoring/api/resources の'Resource type Display name'
      & mrLabels ?~ resourceLabels
    labels = writeLogEntriesRequestLabels $ HM.fromList -- (2) これは任意
      [ ("key", "value")
      ]

  runResourceT . runGoogle env $
    send
      (entriesWrite
        (writeLogEntriesRequest
          & wlerEntries .~ entries
          & wlerLogName ?~ logName
          & wlerResource ?~ resource
          & wlerLabels ?~ labels
        )
      )

実行結果

コンソール

コンソール上でプログラムを実行すると、このように出力された。(隠したいところは...と加工しているので、出力そのままではない)

$ ./google-log logid ログメッセージ
Writing log message...
("projectId","project-id")
("description","")
("hostname","...internal")
("instanceId","53...46")
("zone","projects/64...11/zones/asia-northeast1-c")
("FromMetadata",ServiceId "default")
[Client Request] {
  host      = logging.googleapis.com:443
  secure    = True
  method    = POST
  timeout   = ResponseTimeoutMicro 70000000
  redirects = 10
  path      = /v2/entries:write
  query     = ?pp=true&alt=json
  headers   = authorization: Bearer ya..ujc; accept: application/json; content-type: application/json
  body      = {"entries":[{"textPayload":"ログメッセージ"}],"resource":{"labels":{"instance_id":"53...46","zone":"projects/64...11/zones/asia-northeast1-c","project_id":"project-id"},"type":"gce_instance"},"labels":{"key":"value"},"logName":"projects/project-id/logs/logid"}
}
[Client Response] {
  status  = 200 OK
  headers = content-type: application/json; charset=UTF-8; vary: Origin; vary: X-Origin; vary: Referer; content-encoding: gzip; date: Sat, 28 Oct 2017 06:20:22 GMT; server: ESF; cache-control: private; x-xss-protection: 1; mode=block; x-frame-options: SAMEORIGIN; x-content-type-options: nosniff; transfer-encoding: chunked
}
( "result" , WriteLogEntriesResponse' )

GCPのコンソール

GCPのコンソールでログを確認できた。

まとめ

今回はログできるかどうかをテストした。ログレベル、ログID、メッセージ、ラベルの連想配列を渡してログを出力する関数を作れば便利だと思うので、次はそれを作りたい。