Grafanaのダッシュボード上にGoogle Calendarを表示する

# 概要

Grafanaのダッシュボード上に、Google Calendar上の予定を表示する方法です。
以下を利用します。

* Google Calendar API を利用して、カレンダーのイベント情報を取得し、JSON 形式で返却するアプリケーション
	* [GCalJSON](https://github.com/watahari/GCalJSON) [^1]
* JSON 形式の API 応答を受け取る Grafana の Data Source Plugin
	* [Infinity data source plugin for Grafana](https://grafana.com/docs/plugins/yesoreyeram-infinity-datasource/latest/)
* Grafana のカレンダーを表示するプラグイン
	* [Business Calendar Plugin \(marcusolsson-calendar-panel\)](https://grafana.com/grafana/plugins/marcusolsson-calendar-panel/)

GCalJSON は安全な場所にホスティングする必要があります。


# in a nutshell

https://github.com/watahari/GCalJSON/tree/main/sample


# 動作イメージ

![GrafanaのダッシュボードにBusiness Calendar Pluginを利用してGoogle Calendarの予定が表示されている様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/8f253d94-2f93-442e-b6ff-b066c0d73b04.png)


上記は月表示の例です。
Business Calendar Plugin は日・週・平日・年の表示や、予定のリスト表示が可能です。

# 構成図

構成図は以下の通りです。

```mermaid
graph LR
    gapi[Google Calendar API]
    subgraph yourNW[your own network]
    	subgraph grafana[Grafana]
    		inf[Infinity data source plugin]
    		cal[Business Calendar Plugin]
    		cal --> inf
    	end
    	gcaljson[GCalJSON]
    	inf --> gcaljson
    end
    gcaljson --> gapi
```

GCalJSONが、Google Calendar API への認証、応答のキャッシュ、データの変換を行います。
Google Calendar API へのリクエストはサービスアカウントで行い、認証情報は環境変数で与えます。

無理やり図にすると、以下の通りです。

```mermaid
graph LR
    gapi[Google Calendar API events/list]
    subgraph yourNW[your own network]
    	subgraph grafana[Grafana App]
    		inf[Infinity data source plugin]
    		cal[Business Calendar Plugin]
    		cal <--> inf
    	end
    	subgraph gcaljson[GCalJSON App]
    		gcaljson-handler[handle request]
    		gcaljson-oauth[OAuth 2.0 Authorization]
    		gcaljson-get[request to G Cal API]
    		gcaljson-cnv[convert data]
    		gcaljson-res[responce data]
    		gcaljson-cache[(Cache)]
    		gcaljson-handler -->|if cache exists| gcaljson-cache
    		gcaljson-cache --> gcaljson-res
    		gcaljson-handler -->|if no cache| gcaljson-oauth
    		gcaljson-oauth --> gcaljson-get
    		gcaljson-get --> gcaljson-cnv
    		gcaljson-cnv --> gcaljson-res
    		gcaljson-cnv -..->|register cache| gcaljson-cache
    	end
    	envvar[Environment Variables<br> Credentials, Calendar ID, Cache setting]
    	gcaljson -->|read| envvar
    	inf <--> gcaljson
    end
    gcaljson <--> gapi
```

# 対象となる利用者のイメージ

* Grafana を独自にホスティングして運用している
* Google Calendar を Grafana のダッシュボード上に表示したい

構成図のとおり、Grafana Cloud を利用するユーザーは念頭にありません。
ただ、良い感じに読み替えていただければ、なんとかなるとは思います。

GCalJSON は、Google Calendar APIのサービスアカウントを利用して、カレンダー情報を応答するAPIです。
つまり、GCalJSON のエンドポイントは無認証でカレンダー情報を応答します。
エンドポイントは安全な場所にホスティングするか、隠し通す必要があります。あるいは、公開してもいいカレンダーのみに限定しましょう。

私の場合は、LAN 内の Raspberry Pi に docker compose で一緒にデプロイしています。
WANからLANを受け付けていない状態、また、念の為、対象のカレンダーには個人情報を書かない状態で運用しています。

# 設定準備

## Google Calendar API をリクエストするサービスアカウントの作成

Google Calendar APIを有効にしたプロジェクトで、サービスアカウントを発行し、クレデンシャルを取得します。

* [Google Cloud Console](https://console.cloud.google.com/)にアクセスしてください。
* 新しいプロジェクトを作成します。既存のプロジェクトがあれば、それを選択してもOKです。
* Google Calendar API を有効にしてください。
* APIとサービス > 認証情報 から、新しいサービスアカウントを作成してください。
* 鍵を作成し、JSON 形式でダウンロードしてください。

詳しくは、以下のドキュメントが参考になるかもしれません。

* [サービス アカウントを手動で作成する - Google Workspace Migrate](https://support.google.com/workspacemigrate/answer/10839762?hl=ja)
	* 手順1で有効にする必要があるAPIは Google Calendar API です
	* 手順3は不要です
* [Google API サービスアカウントファイル作成・設定手順 &ndash; Cloud - Flight Attendant](https://clo-support.flight.co.jp/hc/ja/articles/14858881486873-Google-API-%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E4%BD%9C%E6%88%90-%E8%A8%AD%E5%AE%9A%E6%89%8B%E9%A0%86)
	* 「Google APIの有効化」>「4. 指定のAPIを検索し、有効化する」では Google Calendar API を有効化してください
	* 手順「ドメインへのAPIスコープの登録」は不要です

ダウンロードした鍵ファイル(JSON)は後ほど使いますので、大切に保管してください。

また、発行したサービスアカウントのメールアドレス `<サービスアカウント名>@XXXXXX.iam.gserviceaccount.com` をメモしておいてください。
Google Cloud Console 上でも確認できますし、ダウンロードした鍵ファイルの `client_email` フィールドにも記載されています。

## Google Calendar の設定

[Google Calendar](https://calendar.google.com/)を開いてください。
Grafanaで表示したいカレンダーの設定画面を開きます。

![Google Calendar の 設定と共有メニュー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/afb4aa6b-a03b-4ba8-b7af-dadf477af94e.png)


「カレンダーの統合」部分に、カレンダーIDが表示されていますので、メモしておいてください。

![Google Calendar 設定の「カレンダーの統合」に表示されたカレンダーID](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/06dc847c-bf88-487e-ba12-142ad8902dc6.png)


次に、先ほど発行したサービスアカウントに、このカレンダーの読み取り権限を付与します。
「共有する相手」部分の「ユーザーやグループを追加」ボタンを押下します。
先ほどメモしておいたサービスアカウントのメールアドレス `<サービスアカウント名>@XXXXXX.iam.gserviceaccount.com` を入力します。
権限は、予定の詳細参照権限である「予定の表示（すべての予定の詳細）」を指定してください。

![Google Calendar の共有先にサービスアカウントのメールアドレスを設定する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/30c74395-885f-46fd-b138-a492033be6e6.png)



# GCalJSON の設定と起動

GCalJSONには、3つの設定値を環境変数で渡すことができます。

* `GCALJSON_GOOGLE_CREDENTIAL` : カレンダー情報を取得するサービスアカウントの鍵（Base64符号化したもの）[^2]
* `GCALJSON_GOOGLE_CALENDAR_ID` : 表示したいカレンダーID
* `GCALJSON_CACHE_DURATION` : キャッシュ時間の設定

## GCALJSON_GOOGLE_CREDENTIAL
まず、先程ダウンロードした鍵ファイル(JSON)をBase64符号化します。

```
base64 -w 0 credentials.json
```

Macであれば、こんな感じでクリップボードにコピーできます。

```
base64 -w 0 -i ~/Downloads/XXXXXXX.json | pbcopy
```


`.env`ファイルに、以下のように記載してください。
```
GCALJSON_GOOGLE_CREDENTIAL='Base64符号化した鍵'
```

## GCALJSON_GOOGLE_CALENDAR_ID

カレンダーの設定画面で、「カレンダーの統合」部分からコピーしたカレンダーIDを、記載してください。

`.env`ファイルに、以下のように追記してください。
```
GCALJSON_GOOGLE_CALENDAR_ID='XXXXXXXXX@group.calendar.google.com'
```

## GCALJSON_CACHE_DURATION (任意)

指定しない場合、5分キャッシュとなります。

例えば、1分キャッシュにしたい場合は、以下のように設定してください。
`.env`ファイルに、以下のように追記してください。
```
GCALJSON_CACHE_DURATION='1m'
```

# docker compose での設定と起動

:::note info
[https://github.com/watahari/GCalJSON/tree/main/sample](https://github.com/watahari/GCalJSON/tree/main/sample)を参照するほうが分かりやすいかもしれません。
:::

## GCalJSONの設定

`docker-compose.yaml` を作成します。

```
version: "3"
services:
  gcaljson:
    image: watahari/gcaljson
    ports:
      - "8080:8080"
    environment:
      - GCALJSON_GOOGLE_CREDENTIAL=${GCALJSON_GOOGLE_CREDENTIAL}
      - GCALJSON_GOOGLE_CALENDAR_ID=${GCALJSON_GOOGLE_CALENDAR_ID}
      - GCALJSON_CACHE_DURATION=${GCALJSON_CACHE_DURATION}
```

`docker compose up -d` でコンテナを起動してください。
`http://localhost:8080/events`で、カレンダーの予定イベントが取得できているはずです。

```
$ docker compose up -d
$ curl "http://localhost:8080/events"
[{"title":"予定のタイトル","start":"2025-02-02T11:00:00+09:00","end":"2025-02-02T14:00:00+09:00"},...中略...]
$ curl -s "http://localhost:8080/events" | jq . | head -15
[
  {
    "title": "予定のタイトル ほげほげ",
    "start": "2025-02-02T11:00:00+09:00",
    "end": "2025-02-02T14:00:00+09:00"
  },
  {
    "title": "予定のタイトル ふがふが",
    "start": "2025-02-05",
    "end": "2025-02-06"
  },
  {
    "title": "予定のタイトル ぴよぴよ",
    "start": "2025-02-06",
    "end": "2025-02-16"
$ docker compose down
```

## Grafana の設定 (データソースを設定ファイルから Provisioning する)

既に、Grafana の docker-compose.yaml は書いてある前提で、話を進めます。

Grafana に [Infinity data source plugin for Grafana](https://grafana.com/docs/plugins/yesoreyeram-infinity-datasource/latest/)と、[Business Calendar Plugin \(marcusolsson-calendar-panel\)](https://grafana.com/grafana/plugins/marcusolsson-calendar-panel/)をインストールする必要があります。
以下のように、`GF_INSTALL_PLUGINS` にプラグイン名を渡してください。

```
services:
  grafana:
    image: grafana/grafana
    environment:
      - GF_INSTALL_PLUGINS=yesoreyeram-infinity-datasource,marcusolsson-calendar-panel
```

次に、DataSourceとして、以下のように設定してください。

```
apiVersion: 1

datasources:
  - name: GCalJSON
    type: yesoreyeram-infinity-datasource
    url: http://gcaljson:8080/events
```

先ほどの docker-compose.yaml への記載によって、gcaljson という名前のコンテナが 8080 番ポートで受け付けています。
urlには、その `/events`エンドポイントを指定します。


### (補足) Grafana 設定をWebUIで実施する場合

データソースを設定ファイルから Provisioning していない場合、つまり、GrafanaのWebUIからデータソースを設定している場合は、以下のように設定してください。


* Grafana にログインし、Data Source 追加画面を開いてください。
* "+ Add new data source" から、Infinity のデータソースを作成します。
	* [Infinity data source plugin for Grafana](https://grafana.com/docs/plugins/yesoreyeram-infinity-datasource/latest/)をインストールしていない場合、インストールしてください。
* Infinify データソースの Settings > URL, Headers & Params から、以下のように設定してください。

![Grafana Infinity データソースに GCalJSON のエンドポイントを設定する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/96779ae3-7d1d-4af2-9e0f-34ec0bfae800.png)


Base URL には、GCalJSONのエンドポイントを指定します。urlには、その `/events`エンドポイントを指定します。
先述の [GCalJSONの設定](#gcaljsonの設定) の通りに`docker-compose.yaml` を記載してGCalJSONを記載しているのであれば、Base URLは `http://gcaljson:8080/events`となります。

# Grafanaダッシュボードを編集し、カレンダーを表示する

:::note info
[Business Calendar 公式ドキュメント](https://volkovlabs.io/plugins/business-calendar/)を読むことを推奨します。
:::

カレンダーを追加したい Grafana ダッシュボードを開き、編集モードに入ってください。
ビジュアライゼーションを追加し、Business Calendar を指定してください。

![Business Calendar を追加する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/f8e2cf35-fefa-421a-9bd1-75bc0e0e0c0c.png)


以下の通りに設定します。

* Table View を ON にする
* Data Sourceに、GCalJSONを参照しているInfinity PluginのData Sourceを指定する
* この状態で refresh すると、カレンダーの予定が表形式として表示されることが確認できるはず
* Business Calendarの右メニュー設定で、Data を以下のように設定する
	* Start time : `start` カラムを指定する
	* End time : `end` カラムを指定する
	* Text : `title` カラムを指定する

以下の画像も参考にしてください。

![Grafana Business Calendar に GCalJSON のカラムを設定する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/a28cf82a-ca07-476f-81d6-fc2fec9b5136.png)


Table View を OFF にすると、カレンダー形式で表示されているはずです。

![Grafana Business Calendar に GCalJSON を設定した様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/602340/3ce8de29-ffae-4814-b78e-10163cc92f3a.png)


# この構成の利点

GCalJSON は、以下の3つの役割を持っています。

* Google Cloud サービスアカウントの OAuth 2.0 認証を行なう
* Google Calendar API をリクエストし、Business Calendar Plugin が扱いやすい JSON に変換して応答する
* キャッシュした応答を提供する（デフォルトでは5分キャッシュ）

それぞれについて、GCalJSON を利用する利点を示します。

### (1) 認証について

Infinity plugin でも OAuth 2.0 認証を通すことができます。[^3]
しかし、GCalJSON を利用することで、認証のためのクレデンシャルを環境変数に隠蔽することができます。

Grafana Cloud を利用している場合や、Provisioning ではなく、手動設定とデータベース上での保存で良い場合には、この利点は関係ありません。

逆に言えば、Grafana のインスタンスを独自で起動して運用する場合、特にデータソースを設定ファイルから Provisioning したい場合は、どうやってクレデンシャルを隠蔽して構成管理を行なうかが重要です。
GCalJSON は、環境変数 `GCALJSON_GOOGLE_CREDENTIAL` に、Google Cloud サービスアカウントのクレデンシャル JSON を base64 化して渡すことができます。よって、構成管理で扱う秘密情報を環境変数に閉じ込めることができます。

### (2) Google Calendar API のイベント情報について

Infinity Pluginで OAuth 2.0 認証を通し、Business Calendar Plugin で、直接 Google Calendar の Events: list API [^4] をリクエストすることもできます。

しかし、その場合は以下の部分で工夫が必要です。

* リクエストパラメータ `timeMin` や `timeMax` を良い感じに絞る必要があります
	* Business Calendar の表示に合わせて対象の期間を指定する？どうやって？
	* テキトーに現在時刻のプラスマイナス1ヶ月程度に指定する？どうやって？
* レスポンス `items` の中身を扱いやすいように変換する必要があります
	* 軽く試した感じだと、 `Parsing options & Result fields` で `Rows/Root` に `items` を指定すればなんとかなりそうに思えました。
	* しかし、各イベントの `start` と `end` には、`date` という日付のみが入った項目がある場合と、`dateTime` という日時が入った項目がある場合がありそうです。
	* Business Calendar は start と end にそれぞれ一つの項目のみを指定するため、難しそうです。
	* 終日イベントのみの場合は、`Parsing options & Result fields` の `Columns` で、 `start.date` as `start` などとすればなんとかなりそうでした。
	* 終日イベントと時刻指定イベントが混在する場合は、、、どうやって・・・？

GCalJSON は、以下のように情報が整形されているため、簡単に Business Calendar の設定が可能です。
```
curl -s "http://XXXXX:8082/events" | jq . | head
[
  {
    "title": "時刻指定の予定",
    "start": "2025-02-01T11:00:00+09:00",
    "end": "2025-02-01T14:00:00+09:00"
  },
  {
    "title": "終日の予定",
    "start": "2025-02-05",
    "end": "2025-02-06"
 ```

### (3) APIのリクエストリミット対策 (キャッシュ)

Google Calendar API のリクエスト制限については、以下のURLに記載があります。

[https://developers.google.com/calendar/api/guides/quota](https://developers.google.com/calendar/api/guides/quota)

* 2025年03月現在、追加料金なしで利用可能です。
* 1分あたりの利用可能枠があるようですが、詳細は公開されていません。（噂によれば 5rps/user のようです。[^5]）
* 枠を超えた場合、 `403` か `429` が応答されます。

よほどのことが無ければ、今回のような用途ではキャッシュは必要がないように思えます。
しかし、ダッシュボードを常時表示して自動更新させている場合や、複数ユーザがダッシュボードを表示する場合には注意が必要です。

そもそもカレンダーをよほど頻繁に更新しているのでも無い限り、ある程度のキャッシュを効かせてしまえば、余計なことを考える必要がなくなります。
GCalJSON はデフォルトで 5分のキャッシュを保持します。
なお、 `GCALJSON_CACHE_DURATION` でキャッシュ時間設定を変更することができます。


# まとめ

Grafanaのダッシュボード上に、Google Calendar上の予定を表示する方法を紹介しました。
Google Calendar API、GCalJSON、Grafana、Grafana Infinity data source plugin、Grafana Business Calendar plugin によって実現しました。

私は、家庭内のスケジュール可視化に利用しています。[^6]
応用例としては、オンコールスケジュールやプロジェクト計画などでの利用も考えられるかと思います。

以上、ご参考まで。


[^1]: GCalJsonは私が提供しているので、OSS公開しているものの、この記事が我田引水であることに注意してください。

[^2]: 環境変数でのJSON受け渡しが面倒なので、Base64符号化してプレーンテキストとして扱っています

[^3]: Google Calendar API を、Infinity Plugin でOAuth 2.0 認証を通して利用したい場合、Settings > Authenticationで 以下のように設定します。
Auth type: OAuth2
Auth details > Grant Type: JWT
Auth details > Email: クレデンシャルJSONの `client_email` を設定する
Auth details > Private Key: クレデンシャルJSONの `private_key` を設定する
Auth details > Token URL: クレデンシャルJSONの `token_uri` を設定する
Auth details > Scopes: [https://developers.google.com/calendar/api/auth?hl=ja](https://developers.google.com/calendar/api/auth?hl=ja) を参考に設定する
Allowed hosts: `https://www.googleapis.com/`

[^4]: https://developers.google.com/calendar/api/v3/reference/events/list

[^5]: https://qiita.com/naka_kyon/items/56c3073aefafdc26b9a2

[^6]: https://github.com/watahari/myHomeAutomation/