selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

Stackdriver Logging を利用して特定の条件に一致したLogの情報をSlackに通知する

最近、Cloud Functionsを利用してサーバレスでシステムを構築しています。けれども、まだまだサーバレスでシステムを作り慣れていないので、Cloud Functionsが正しく実行されたのか、ちょっと不安なので確認したいと考えていました。そこで、Cloud Functionsが実行完了した際に、それを検知してSlackにメッセージを送信する仕組みを作ってみたので、その説明をします。

TL;DR

  • Cloud Functionsが実行完了した際に通知してくれる仕組みを作りたかった
  • Cloud Functionsの実行ログが流れる stackdriver loggingを利用して、フィルタを設定する
  • フィルタに一致するログを受け取ったら pub/subでメッセージを送信
  • 上のメッセージをフックにしてSlack通知を行う Cloud Functionを実装
  • それによって目的を達成するシステムを実装した

システムの全体像

f:id:selmertsx:20190325202219p:plain

Stackdriver Loggingのログエクスポート機能を用いて、指定したフィルタに一致するログを取得しCloud Pub/Subでメッセージとして流します。そのメッセージを受け取った Cloud FunctionsがSlackに通知します。

前提知識

Stackdriver Loggingについて

https://cloud.google.com/logging/?hl=ja

Stackdriver Loggingはログデータやイベントを格納、検索、分析、モニタリング、通知するためのサービスです。Stackdriver LoggingのAPIを使えば、あらゆるソースからデータを取り込むことができます。GCPのサービスは、基本的にStackdriver Loggingにログが送られるようになっており、すぐに利用することができます。

Stackdriver Loggingではすべてのクラウドログを一箇所に集めて管理します。そのためそれら膨大な種類のデータの中から、価値のあるデータを拾い集め、適切な処理を促す仕組みが充実しています。今回はその中のExport機能を利用してCloud Functionsの実行通知をすることにしました。

Stackdriver LoggingのExport機能について

https://cloud.google.com/logging/docs/export/?hl=ja

Stackdriver Loggingには、ログをエクスポートする機能があります。エクスポートする用途としては下記の項目が挙げられます。

  • ログを長期間保存するため。(通常のログは30日間程度保持されます)
  • ログを分析するため
  • 他のアプリケーションで利用するため

上記の用途で利用できるようにするため、Stackdriver Loggingでは Cloud Storage, BigQuery, Cloud Pub/Subの3つにログをエクスポートすることができます。

StackdriverでLogをエクスポートする設定を理解するためにはsinkと呼ばれるオブジェクトについて理解する必要があります。Sinkオブジェクトは、自身の名前、エクスポートするログを選択するためのフィルタ、そしてフィルタに引っかかったログのエクスポート先の3つの要素から構築されます。

Logging自体には、ログをエクスポートする上で料金や制限は存在しません。けれども、エクスポート割きでのログデータの保存や送信には料金が掛かります。

Sinkはproject, organizations, billing_accounts単位で設定できます。そのため、監査ログを一元管理したいときなどはorganizationsに対してSinkの設定をするなど、用途に応じて適用する範囲を設定する必要があります。

設定と実装

要件

sample_cloud_function という名前の cloud functionsが実行完了した際に、その実行完了のログを検知してSlackに通知する」という要件で Stackdriver LoggingのログをSlackに通知してみます。

フィルタの設定

フィルタの設定については、こちらのページに設定方法が記載されています。設定に利用できるプロパティについては、こちら に記載されています。なお、resource type毎のlabelの値については、資料を見つけることができなかったので記載していません。

上記資料をもとにフィルタを自分で作成してみます。今回はsample_cloud_functionという名前のcloud functionが実行を完了したら、任意のpub/sub topicにメッセージを送信する必要があります。その要件を満たすフィルタは下記のようなります。

resource.type = "cloud_function"
resource.labels.region = "asia-northeast1"
resource.labels.function_name = "sample_cloud_function"
textPayload: "finished"

なおフィルタ設定時のベストプラクティスについては、下記のように記述されています。僕は毎回判断することが手間だったので、必ず引用符を用いるようにしています。

ベスト プラクティス: フィールド値と比較する文字列は引用符で囲むようにしてください。これにより、比較の意味が変わったり、デバッグが困難になるような誤りを防ぐことができます。文字の後に連続した文字、数字、アンダースコア(_)が続く単語の場合は、引用符を省略できます。

エクスポート先の設定 [terraform]

https://www.terraform.io/docs/providers/google/r/logging_project_sink.html

今回はPub/Subの cloud-functions-activity というTOPICに対してメッセージを送信することにしています。上記terraformのドキュメントを見ると、下記のように設定すると書かれています。

destination - (Required) The destination of the sink (or, in other words, where logs are written to). Can be a Cloud Storage bucket, a PubSub topic, or a BigQuery dataset. Examples: "storage.googleapis.com/[GCS_BUCKET]" "bigquery.googleapis.com/projects/[PROJECT_ID]/datasets/[DATASET]" "pubsub.googleapis.com/projects/[PROJECT_ID]/topics/[TOPIC_ID]" The writer associated with the sink must have access to write to the above resource.

unique_writer_identity については、projectをまたいで利用する場合はtrueにする必要があります。将来的に複数のprojectのcloud functionsのlogをここで通知したいと考えているので、ここでは true としました。

resource "google_logging_project_sink" "sample_cloudfunction" {
    name = "sample_cloudfunction_sink"
    destination = "pubsub.googleapis.com/projects/xxx/topics/cloud-functions-activity"
    filter = "resource.type = 'cloud_function' AND resource.labels.region = 'asia-northeast1' AND resource.labels.function_name = 'sample_cloudfunction' AND textPayload: 'finished'"
    unique_writer_identity = true
}

デプロイの設定 [cloudbuild]

cloudbuildを利用してデプロイしているので、その設定を載せておきます。とはいえ、重要なポイントは --trigger-topic のオプションの引数が、エクスポート先の設定で指定した cloud-functions-activity というトピックを指定しているという点のみです。

- name: 'gcr.io/cloud-builders/gcloud'
    args:
    - beta
    - functions
    - deploy
    - slack_reporter
    - --region=asia-northeast1
    - --stage-bucket=cf-bucket-for-xxx
    - --trigger-topic=cloud-functions-activity
    - --runtime=nodejs8

Pub/Sub Messageの内容

https://cloud.google.com/logging/docs/export/using_exported_logs?hl=ja#pubsub-organization

上記ドキュメントによると、Cloud Pub/Subによってストリーミングされるログは、下記のようなフォーマットになります。

{
 "receivedMessages": [
  {
   "ackId": "dR1JHlAbEGEIBERNK0EPKVgUWQYyODM...QlVWBwY9HFELH3cOAjYYFlcGICIjIg",
   "message": {
    "data": "eyJtZXRhZGF0YSI6eyJzZXZ0eSI6Il...Dk0OTU2G9nIjoiaGVsbG93b3JsZC5sb2cifQ==",
    "attributes": {
     "compute.googleapis.com/resource_type": "instance",
     "compute.googleapis.com/resource_id": "123456"
    },
    "messageId": "43913662360"
   }
  }
 ]
}

dataフィールドを base64でデコードすると、下記のようなLogEntry オブジェクトが取得できます。Cloud Functionの中では下記のObjectを利用してSlackへ送信するメッセージを構築します。

{
  "log": "helloworld.log",
  "insertId": "2015-04-15|11:41:00.577447-07|10.52.166.198|-1694494956",
  "textPayload": "Wed Apr 15 20:40:51 CEST 2015 Hello, world!",
  "timestamp": "2015-04-15T18:40:56Z",
  "labels": {
    "compute.googleapis.com\/resource_type": "instance",
    "compute.googleapis.com\/resource_id": "123456"
  },
  "severity": "WARNING"
}

Cloud Functionの実装

Pub/Sub Messageの内容で確認したObjectを利用してSlackに通知するスクリプトは下記のようになります。このとき SlackのTokenやChannel ID については、こちらの公式ドキュメント に記載されている方法で行っていますが、今回は省略させていただきます。

//index.ts
export async function slack_reporter(data: any) {
  const dataBuffer = Buffer.from(data.data, "base64");
  const logEntry = JSON.parse(dataBuffer.toString("ascii"));
  const client = await SlackClient.create();
  await client.post(`cloud function: ${logEntry.resource.labels.function_name} textPayload: ${logEntry.textPayload}`);
}
// SlackClient.ts
import { WebAPICallResult, WebClient } from "@slack/client";

export class SlackClient {
  public static async create(): Promise<SlackClient> {
    if (!this.instance) {
      this.instance = new SlackClient();
    }
    return this.instance;
  }

  private static instance: SlackClient;

  private slackCleint: WebClient;
  private readonly channel: string = process.env.CHANNEL_ID as string;
  private readonly token: string = process.env.SlackToken as string;

  constructor() {
    this.slackCleint = new WebClient(token);
  }

  public async post(text: string): Promise<WebAPICallResult> {
    return this.slackCleint.chat.postMessage({
      username: "ERP-HR Bot",
      channel: channel,
      text,
    });
  }
}

結果

f:id:selmertsx:20190325202416p:plain

ということで sample_cloud_functionsの実行完了を検知してSlack通知する仕組みを作ることができました。今後は、severity のレベルに応じてメッセージの内容を変更し、迅速に対応が必要なものがあれば即座に分かるようなところまで作ろうかなーとか考えたりしています。

所感

Stackdriver Logging、めちゃくちゃ便利です。一度 Stackdriver Loggingに集約することによって、ログの処理を一元管理することができます。一元管理することで、ログの前処理やBigQueryへのインポート等々、様々な処理を共通化できる気配を感じます。今回はこのような使い方をしましたが、RailsのLogなどを全てBigQueryに入れてしまって、BigQueryで分析することなどもできるのではないかな〜と思ったりしてます(昔Amazon Athenaでやってて、それなりに便利だった)。

もっとガシガシ使って、ポテンシャルを引き出していこう。

SlackのBlock Kitをjsxの記法で書ける jsx-slackを試してみた

TL;DR

  • 僕は個人的に datadog_slack_reporterというものを作成して、datadogで監視しているサービスの台数をslackに通知しています
  • Slackのメッセージ作成部分を、SlackのBlock Kitをjsxの方式で記述できる jsx-slack に置き換えてみました
  • 面倒なjsonの作成部分がReactっぽく書けるので、めちゃくちゃ便利でした
  • この資料にはjsx-slackをTypeScriptで導入する上で必要な設定方法を記載します

jsx-slackとは何か

Slackのメッセージ作成をjsxのフォーマットで行うことができるnpmのpackageです。これを利用すれば、Reactのcomponentを書くような書き味で複雑なjsonの作成をすることができます。

スクリーンショット 2019-03-04 12.07.34.png

上の図はjsx-slack内で生成されるjsonが確認できるページです。こちらの右側のjsonがslackが要求するjsonのフォーマットとなっており、左側がjsx-slackを利用して記述するコードになります。jsx-slackの動作確認ページはこちらになります。

成果物

最初に、今回作成したものの全体像をざっくり説明します。

Slackへのメッセージ出力

スクリーンショット 2019-03-04 11.22.05.png

こちらが、今回僕が作成したbotが実際にslackへ投稿したメッセージです。

ソースコード (一部抜粋)

jsx-slackを使った slackへのメッセージ送信ボットのプログラムがこちらになります。全体像を掴むために、さらっと眺めてもらえれば大丈夫です。

Cloud Functionsのhandler

import moment from "moment-timezone";
import "source-map-support/register";
import { DatadogHostMetrics } from "./datadog";
import { DatadogClient } from "./DatadogClient";
import { SlackClient } from "./SlackClient";
import { slackMessageBlock } from "./SlackMessageBlock";

const datadogClient = new DatadogClient();
const slackClient = new SlackClient();

export async function datadog_handler(data: any): Promise<void> {
  const fromTime = moment({ hour: 0, minute: 0, second: 0 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");

  const toTime = moment({ hour: 23, minute: 59, second: 59 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");
  // datadogのAPIを叩いてデータを取ってくる
  const hostMetrics: DatadogHostMetrics[] = await datadogClient.countHosts(fromTime, toTime);
  // 送信するメッセージを生成する
  const blocks = slackMessageBlock(fromTime, toTime, hostMetrics);
  // メッセージを送信する
  await slackClient.post(blocks);
}

messageの作成部分 (jsx-slackで書き直された場所)

/** @jsx JSXSlack.h */
import JSXSlack, { Block, Section } from "@speee-js/jsx-slack";
import moment from "moment-timezone";
import { DatadogHostMetrics } from "./datadog";
import { ProductMetrics } from "./ProductMetrics";

export function slackMessageBlock(fromTime: string, toTime: string, hostMetrics: DatadogHostMetrics[]) {
  const messages = [];
  for (const metrics of hostMetrics) {
    const productMetrics = new ProductMetrics(metrics);
    const message = (
      <blockquote>
        <b> {productMetrics.name} </b>
        <br />
        min:${productMetrics.minHostCount()} ~ max:${productMetrics.maxHostCount()}
        sum(host*hours):${productMetrics.sum()}
      </blockquote>
    );
    messages.push(message);
  }

  return JSXSlack(
    <Block>
      <Section>
        <p>datadog monitoring daily report</p>
        {moment.unix(parseInt(fromTime, 10)).toString()} ~ {moment.unix(parseInt(toTime, 10)).toString()}
        {messages}
      </Section>
    </Block>
  );
}

messageの送信部分

import { WebClient } from "@slack/client";

export class SlackClient {
  private static username: string = "Datadog按分計算Bot";
  private readonly channelID: string = process.env.CHANNEL_ID as string;
  private readonly token: string = process.env.SlackToken as string;
  private readonly client: WebClient;

  constructor() {
    this.client = new WebClient(this.token);
  }

  public post(blocks: any) {
    return this.client.chat.postMessage({
      channel: this.channelID,
      text: "",
      blocks,
      username: SlackClient.username,
    });
  }
}

実際のコードはこちらを参照してください。 https://github.com/selmertsx/datadog_slack_report/pull/4

基本的な書き方

  • 最初にslack本家のblock-kit-builderで、サンプルを見ながらどのようなblock kitが作りたいのか考えます。
  • jsx-slackのリポジトリ内にはサンプルがいくつかあるので、そちらを参照しても良いでしょう
  • その後、jsx-slackのサイトで目的のjsonが生成できる記法を探します。

jsx-slackの導入設定

TypeScriptで導入する際は少しだけ設定をする必要があります。公式ドキュメントを見ると下記のような記載があります。

https://github.com/speee/jsx-slack

A prgama would work in Babel (@babel/plugin-transform-react-jsx) and TypeScript >= 2.8 with --jsx react.

ということで、jsx-slackをTypeScriptで利用するにはtsconfigの設定が必要であることが分かります。以降、それらの設定を行っていきましょう。

@speee-js/jsx-slackのinstall

npm i @speee-js/jsx-slack

今回の設定に必要なpackageを上記コマンドで全部installします。

tsconfigの設定

TypeScriptのjsxに関するドキュメントを読んでみましょう。

https://www.typescriptlang.org/docs/handbook/jsx.html#factory-functions

The exact factory function used by the jsx: react compiler option is configurable. It may be set using either the jsxFactory command line option, or an inline @jsx comment pragma to set it on a per-file basis.

上記コメントで示されているように、TypeScriptでjsxを利用するためには、ファイルごとのpragmaの設定と、コンパイラオプションの指定が必要になります。pragmaの設定とは、サンプルコードで示されていた /** @jsx JSXSlack.h */ の部分を指します。コンパイラオプションは、下記のように設定します。

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": [
      "es5",
      "es2015",
      "es2016.array.include",
      "esnext.asynciterable"
    ],
    "strict": true,
    "outDir": "./",
    "esModuleInterop": true,
    "noImplicitAny": true,
    "allowJs": true,
    "jsx": "react" // <= 今回追加した設定
  },
  "include": [
    "src/**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "__tests__",
    "**/__mocks__/*.ts"
  ]
}

実際のコードはこちら

最後に

jsx-slack 導入の手順は以上になります。今回の例はちょっとシンプルすぎるのであまり有り難みを感じにくいかも知れませんが、もうちょっと複雑になってくると jsxを利用してReactっぽく書ける jsx-slackに大きなメリットを感じられるかと思います。

Sidekiqのjobの信頼性向上方法と Sidekiq Proの検討について

自分のための覚書

TL;DR

  • sidekiq proでは、server processが死んでも jobの復活がサポートされる
  • sidekiq proにおいて、redis が死んでも、1000件程度のジョブならclientが保持し続けて、redisが復活したタイミングでenqueue してくれる
  • ↑の状況において、client processが死ねば、蓄積された1000件のジョブは全て消える
  • sidekiq enterpriseでは、unique な jobになるように諸々やってくれるが、完全に保証してくれるものでは無いので、そこんところを考えたjob設計にすること。

Pricing

  • Pro
    • $9,500 / yr
  • Enterprise
    • 250 thread $19,500 / yr
    • 500 thread $23,400 / yr

Jobが失われないようにする仕組みについて

sidekiqにおいてjobが失われるケースは下記の3点

  • redisに接続できないケース
  • client processが死んだケース
  • server processが死んだケース

この中でSidekiq Proが対策をしているのは redisに接続できないserver processが死んだ の2つのケース。

redisに接続できない場合

基本的に、下記の資料に書いてある内容。 https://github.com/mperham/sidekiq/wiki/Pro-Reliability-Client

通常版だと、redisが死んでいたらjobがpush出来ない。しかしPro版だとpushできなかったjobをclientのプロセスが保持しており、特定のjobがpushできるようになったタイミングで残っているjobも一緒にpushしてしまう。なお、client が保持できるjobの数は最新の1000件のみ。1000件を越えた場合は、永遠に失われてしまうので、そこは気をつけること。

server processが死んだ場合

  • sidekiq basicは、Redis queueからのjobのフェッチにBRPOPを使っている
  • sidekiqがjobを実行している最中にクラッシュしたら、そのjobは永遠に失われてしまう
  • jobが失われない様に保証するための唯一の方法は、jobが完了するまでredisから削除しないこと
  • sidekiq proはRedisのPROPLPUSH コマンドを利用してそれを実現している
  • BROPLPUSH を使う場合は下記のように記述する必要がある
Sidekiq::Client.reliable_push! unless Rails.env.test?

Sidekiq.configure_server do |config|
  config.super_fetch!
  config.reliable_scheduler!
end

なお、super_fetchをする際は、redisデータベースの全走査を行うので、cache dataとjob dataで保持するredisを分けるのが望ましい。

JobのUnique制約について

大前提として jobは (class, 引数, queue)を組み合わせてuniqueであることが求められる。同じ引数のjobを異なるqueueに入れることができる。そのため、同一引数のjobで違う結果を期待してはいけない。基本的に unique な制約は時間に掛けることになる。

class MyWorker
  include Sidekiq::Worker
  sidekiq_options unique_for: 10.minutes

  def perform(...)
  end
end

上記のような設定をした場合、jobが成功するか、jobが失敗して10分が経過するまで同一引数のjobを実行することは出来ない。

Office365のユーザーimport/exportはそれぞれフォーマットが異なる

TL;DR

  • AzureADのデータについて、ロールバックできる仕組みが欲しい
  • できる限り自前で実装せずに、公式の提供してくれる仕組みに乗っかりたい
  • Office365に存在する import/exportを試してみた
    • そのままでは IDaaSのロールバック用途には使えなさそう
    • グループのexportがない
    • importとexportでデータのフォーマットが異なる

方法

export

f:id:selmertsx:20190212172626p:plain
Azure Portal

https://portal.office.com/AdminPortal/Home#/users

上記URLのエクスポートボタンを押すと、下記のようなフォーマットのcsvをダウンロードできます。 下記の項目のデータが、従業員の人数分得られます。

AlternateEmailAddresses,BlockCredential,City,Country,Department,DisplayName,Fax,FirstName,LastDirSyncTime,LastName,LastPasswordChangeTimestamp,LicenseAssignmentDetails,Licenses,MobilePhone,OathTokenMetadata,ObjectId,Office,PasswordNeverExpires,PhoneNumber,PostalCode,PreferredDataLocation,PreferredLanguage,ProxyAddresses,ReleaseTrack,SoftDeletionTimestamp,State,StreetAddress,StrongPasswordRequired,Title,UsageLocation,UserPrincipalName,WhenCreated

import

f:id:selmertsx:20190212172908p:plain
import button

importは上記URLから「その他」「複数のユーザーのインポート」を選択すると、設定することができます。

importする際に必要なデータのフォーマットについて、この資料 を参考に見てみると、下記のようなフォーマットのデータを求めていると書かれています。

User Name,First Name,Last Name,Display Name,Job Title,Department,Office Number,Office Phone,Mobile Phone,Fax,Address,City,State or Province,ZIP or Postal Code,Country or Region
chris@contoso.com,Chris,Green,Chris Green,IT Manager,Information Technology,123451,123-555-1211,123-555-6641,123-555-9821,1 Microsoft way,Redmond,Wa,98052,United States
ben@contoso.com,Ben,Andrews,Ben Andrews,IT Manager,Information Technology,123452,123-555-1212,123-555-6642,123-555-9822,1 Microsoft way,Redmond,Wa,98052,United States
david@contoso.com,David,Longmuir,David Longmuir,IT Manager,Information Technology,123453,123-555-1213,123-555-6643,123-555-9823,1 Microsoft way,Redmond,Wa,98052,United States
cynthia@contoso.com,Cynthia,Carey,Cynthia Carey,IT Manager,Information Technology,123454,123-555-1214,123-555-6644,123-555-9824,1 Microsoft way,Redmond,Wa,98052,United States
melissa@contoso.com,Melissa,MacBeth,Melissa MacBeth,IT Manager,Information Technology,123455,123-555-1215,123-555-6645,123-555-9825,1 Microsoft way,Redmond,Wa,98052,United States

exportされたフォーマットと比べてみると、項目が大きく異なることがわかります。 importするには、諸々データを加工してimport用のデータを作成し、consoleからuploadします。 このとき、200名を超えるデータは一度にimportすることはできません。 また、importする際のCSVのカラムについて、過不足があると登録することができません。

以上のことから、exportで取得したデータを、importにそのまま使えないというちょっとびっくりさせられる仕様のOffice365さんでした。

webpackをupdateしたらterserの問題でbuildができなくなる問題の対処法

TL;DR

  • webpackのバージョンを v4.29.0 にupdateしたらbuildできなくなった
  • どうやら最新の terser-js に問題があったらしい
  • terser-js3.14 にしたら問題が解決した

事象

  • npm update をして webpackのバージョンを v4.29.0 にした
  • その状態で webpack --mode production をしたら下記のエラー
ERROR in index.js from Terser
TypeError: Cannot read property 'minify' of undefined
    at minify (xxx/node_modules/terser-webpack-plugin/dist/minify.js:175:23)
    at module.exports (xxx/node_modules/terser-webpack-plugin/dist/worker.js:13:40)
    at handle (xxx/node_modules/worker-farm/lib/child/index.js:44:8)
    at process.<anonymous> (xxx/node_modules/worker-farm/lib/child/index.js:51:3)
    at emitTwo (events.js:126:13)
    at process.emit (events.js:214:7)
    at emit (internal/child_process.js:772:12)
    at _combinedTickCallback (internal/process/next_tick.js:141:11)
    at process._tickCallback (internal/process/next_tick.js:180:9)
  • terser-webpack-plugin が怪しいらしかったので、そのリポジトリを追っていくとこちらのissueを発見。
  • npm i terser@3.14 してからbuildしなおしたら問題なく動いた

【GCP】Datastoreを作ってしまったGCPプロジェクトではFirestoreを使えない

表題の通り。それだけなんだけれども、同じ悲劇を経験する人がいないようにブログに書きました。

2019年1月、Firestoreが東京リージョンでも使えるようになった。 https://firebase.google.com/docs/firestore/locations?hl=en#location-r

せっかくなので、今作っているcloud functionsのデータベースをDatastoreではなくてFirestoreで構築しようと考えた。( FirestoreはDatastoreの上位互換であり、Datastoreで使える機能はFirestoreでも利用可能 )

ということでドキュメントを見ながらコードを書いて、動作確認をしようとしたら下記のエラー。

{ Error: 9 FAILED_PRECONDITION: This project contains a Cloud Datastore database and does not support Cloud Firestore API calls.
    at Object.exports.createStatusError (/Users/shuhei.morioka/project/speee/datadog_slack_report/node_modules/grpc/src/common.js:91:15)
    at Object.onReceiveStatus (/Users/shuhei.morioka/project/speee/datadog_slack_report/node_modules/grpc/src/client_interceptors.js:1204:28)
    at InterceptingListener._callNext (/Users/shuhei.morioka/project/speee/datadog_slack_report/node_modules/grpc/src/client_interceptors.js:568:42)
    at InterceptingListener.onReceiveStatus (/Users/shuhei.morioka/project/speee/datadog_slack_report/node_modules/grpc/src/client_interceptors.js:618:8)
    at callback (/Users/shuhei.morioka/project/speee/datadog_slack_report/node_modules/grpc/src/client_interceptors.js:845:24)
  code: 9,
  metadata: Metadata { _internal_repr: {} },
  details: 'This project contains a Cloud Datastore database and does not support Cloud Firestore API calls.' }

ここで1~2時間くらい原因を調査したが分からず、issue trackerを見てみたらこんな文言が。

If you absolutely want to use Firestore proper, you need to open a new project and deploy your app to the new project, where you choose Firestore from the start.

https://issuetracker.google.com/issues/113075718

ということで、Datastoreをすでに使っているプロジェクトではFirestoreは使うことが出来ません。これから触る人は、誤って使わないように気をつけましょう〜

ちなみに

Cloud Firestore への自動アップグレード  |  Cloud Datastore ドキュメント  |  Google Cloud

Cloud Firestore の一般公開後、ある程度の時間を置いてから既存の Cloud Datastore データベースの所有者に連絡し、Cloud Firestore の Datastore モードへの自動アップグレードのタイミングを相談させていただく予定です。

ということです!