始めようと思ったキッカケ
上記動画にて、
- 「WHOOP(フィットネストラッカー)のデータを自身のインフラ基盤に取り込む」
- 「サービス側が終了しても、自分のデータは自分で管理したい」
という話が面白く感じた。
今回の目的
外部データをDBに保存し、Grafanaで可視化する流れを理解
AIは補助として使用、公式仕様やトライ&エラーで検証を進める
参考にした公式ドキュメント
Grafana × MySQL
Grafana公式
https://grafana.com/docs/grafana/latest/datasources/mysql/
→ GrafanaがMySQLをデータソースとして扱えることを確認。
Fitbit OAuth2
Fitbit公式
https://dev.fitbit.com/build/reference/web-api/authorization/
→ Fitbit APIがOAuth2ベースで認証されることを確認。
※具体的な項目は以下
・day:その日のデータ
・fitbit_user_id:ユーザ識別
・log_id:Fitbit側のログを特定するため
・is_main_sleep:1日1行としてログ保存するため(true判定)
・合計:deep_minutes, deep_count, light_minutes, light_count, rem_minutes, rem_count, wake_minutes, wake_count (可視化用)
・ステージ:deep/light/rem/wakeのminutesとcount(可視化用)
・更新検知:updated_at,ingested_at(取り込み時刻)
※MySQLのUNIQUE制約で重複防止を図る
・UNIQUE(fitbit_user_id, day) ← 1日1行を守る
・UNIQUE(fitbit_user_id, log_id) ← 同ログ重複防止
・INDEX(fitbit_user_id, day) ← 期間クエリ高速化(Grafana向け)
Fitbit Sleep API
Fitbit公式
https://dev.fitbit.com/build/reference/web-api/sleep/get-sleep-log-by-date/
→ 必要なJSONを確認
事前準備
1. Docker Desktop インストール
Docker Desktop をインストールして起動しておく。
2. Fitbit Developer アカウント作成
3. Grafana アカウント作成
作業フォルダ作成
今回は Cドライブ直下に work フォルダを作成。
mkdir C:\work
cd C:\work
mkdir fitbit-mysql-grafana
cd fitbit-mysql-grafana
Step1 MySQLをDockerで起動してみる
まずは単体で MySQL コンテナを起動し、
Docker上で動作することを確認する。
docker run --name fitbit-mysql `
-e MYSQL_ROOT_PASSWORD=ルート用パスワードを入力 `
-e MYSQL_DATABASE=fitbit `
-e MYSQL_USER=app `
-e MYSQL_PASSWORD=App用パスワードを入力 `
-p 3306:3306 `
-d mysql:8.0
Dockerコマンドの内容
| オプション | 内容 |
|---|---|
| docker run | コンテナ起動コマンド |
| --name | コンテナ名を付与 |
| -e | 環境変数 |
| MYSQL_ROOT_PASSWORD | 必須環境変数 |
| MYSQL_DATABASE | スキーマを自動作成 |
| MYSQL_USER | アプリ用ユーザを作成 |
| MYSQL_PASSWORD | appユーザPW |
| -p 3306:3306 | ポート紐づけ(MySQL標準) |
| -d | バックグラウンド実行 |
コンテナ確認
docker ps
NAMESに「fitbit-mysql」が表示されればOK。
Step2 docker compose化
・ファイルを固定してまとめて起動する仕組み
・MySQLとGrafanaを「この設定で動かす」と.YAMLに書いておく
→台本として用意しておく意味
フォルダ構成作成、確認
mkdir db
mkdir db\init
fitbit-mysql-grafana/
│
├─ compose.yaml
├─ .env
│
└─ db/
└─ init/
└─ 001_schema.sql
・compose.yaml → 構成定義
・.env → 秘密情報
・db/init → MySQL公式仕様で自動実行されるフォルダ
.env 作成
・docker compose up実行時に読み込まれる。
・パスワード直書きを避ける
cd C:\work\fitbit-mysql-grafana
New-Item .env
メモ帳で開いて、以下入力して保存する。
MYSQL_ROOT_PASSWORD=ルート用パスワードを入力
MYSQL_DATABASE=fitbit
MYSQL_USER=app
MYSQL_PASSWORD=App用パスワードを入力
compose.yaml 作成
New-Item compose.yaml
メモ帳で開いて、以下入力して保存する。
services:
mysql:
image: mysql:8.0
container_name: fitbit-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./db/init:/docker-entrypoint-initdb.d
grafana:
image: grafana/grafana:latest
container_name: fitbit-grafana
restart: unless-stopped
ports:
- "3000:3000"
depends_on:
- mysql
volumes:
mysql_data:
・services→起動するコンテナ一覧を書く場所
・environment→先ほど用意した.envを参照
・volumes→データ永続化に推奨
・/docker-entrypoint-initdb.d→配下の .sql を初回起動時に自動実行する仕様となっている。
Step3 sleep_daily テーブル作成
cd work\fitbit-mysql-grafana\db\init
New-Item 001_schema.sql
メモ帳で開いて、以下を入力する。
CREATE TABLE IF NOT EXISTS sleep_daily (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
day DATE NOT NULL,
fitbit_user_id VARCHAR(64) NOT NULL,
log_id BIGINT UNSIGNED NOT NULL,
is_main_sleep BOOLEAN NOT NULL,
deep_minutes INT NOT NULL,
deep_count INT NOT NULL,
light_minutes INT NOT NULL,
light_count INT NOT NULL,
rem_minutes INT NOT NULL,
rem_count INT NOT NULL,
wake_minutes INT NOT NULL,
wake_count INT NOT NULL,
updated_at DATETIME NULL,
ingested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uq_user_day (fitbit_user_id, day),
UNIQUE KEY uq_user_log (fitbit_user_id, log_id),
KEY idx_user_day (fitbit_user_id, day)
);
・CREATE TABLE IF NOT EXISTS→sleep_dailyというテーブルを作成します。IF =重複してなければ作成させる。
・AUTO_INCREMENT →値が連番になる
・log_id →Fitbit側のログ識別
・is_main_sleep →1日1行にするためのtrue検知
・INT →Grafanaで扱いやすい型
・NOT NULL →空では登録させない
Step4 docker compose 起動
docker compose up -d
ここで、ContainerがErrorになります。
原因は、Step1で行った「docker run」で、既に作成したからです。
なので、以下のコマンドで消して、再度コマンドを打てば成功します。
docker rm -f fitbit-mysql
起動確認
docker ps
以下2つが表示されればOK。
- fitbit-mysql
- fitbit-grafana
Step5 MySQLログイン確認
docker exec -it fitbit-mysql mysql -uapp -p fitbit
・docker exec →起動中のコンテナ内で実行
・-it →コンソールに結果を出力
・-uapp →appユーザでログイン
・-p →PW入力宣言
パスワード入力後、
SHOW TABLES;
sleep_daily が表示されれば成功。
Step6 Grafana接続
http://localhost:3000
1.ブラウザで上記にアクセスして、
・username →admin
・passwaord →admin
2.Data sources →MySQLを選択
3.Host URL →mysql:3306
4.Database name →fitbit
5.Username →app
6.Password →.envのもの
7.test&saveをクリック → connection OK と表示される。
【Grafanaの設定】
・Explore→時間範囲をLast 30 daysに変更。
・format:tabelをtime seriesに変更。
・code画面に切り替えて、以下を入力、グラフが出るか確認、
無事グラフ化出来たら、Dashboardへ保存する。
SELECT UNIX_TIMESTAMP(day) AS time, deep_minutes AS value, 'deep' AS metric
FROM sleep_daily
UNION ALL
SELECT UNIX_TIMESTAMP(day) AS time, light_minutes AS value, 'light' AS metric
FROM sleep_daily
UNION ALL
SELECT UNIX_TIMESTAMP(day) AS time, rem_minutes AS value, 'rem' AS metric
FROM sleep_daily
UNION ALL
SELECT UNIX_TIMESTAMP(day) AS time, wake_minutes AS value, 'wake' AS metric
FROM sleep_daily
ORDER BY time;
※コードの内容
sleep_dailyテーブルから日付を「Grafana用の時刻形式」に変換し、deep sleep(深い睡眠時間)を取得して、日付順に並べる
・SELECT →データの指定
・UNIX_TIMESTAMP(day) →UNIX時間に変更=GrafanaがUnix時間採用してる
・As time →列の名前をtimeにする。(Unix時間にあたる)
・deep_minutes AS value →deep_minutes列をvalueで取得
(GrafanaのX軸=time、Y軸=value)
・FROM sleep_daily →自分で作ったテーブル名から取得
・UNION ALL →結果を縦に結合する
・ORDER BY time →日付ソート実施
Step7 Fitbit Developer設定
Fitbit Developer Portal にログイン。
アプリ作成を行う。
重要設定
OAuth 2.0 Application Type
Personal
を選択。
設定が適切でない場合、OAuth認証エラーとなるケースがある。
Redirect URL
http://localhost:8080/callback
を設定。
Scope
最低限以下を許可。
※今回は睡眠データだけほしいので、その他の許可をしない最小権限の考え
sleep
Step8 アクセストークン取得
今回は以下記事を参考。
OAuth2について
Fitbit API は OAuth2 認証方式。
アクセストークンを利用して APIへアクセスする。
Step9 Postmanで睡眠ログを取得
1.POSTMANでアカウントを作成(Githubアカウントでログイン可)
2.new→https→URL欄に以下のFitbit公式APIの Get Sleep Log を入力
https://api.fitbit.com/1.2/user/-/sleep/date/{date}.json
3.Method:GET、Authorization:Bearer Token、Token:アクセストークンを入力
4.sendをクリックすると、JSONでsleepログが返ってくる
※メモ:Fitbit APIはOAuth2.0採用=OAuth 2.0標準形式=Bearer
確認した主なJSON
| 項目 | 用途 |
|---|---|
| dateOfSleep | 日付 |
| logId | ログ識別 |
| isMainSleep | メイン睡眠判定 |
| levels.summary.deep.minutes | deep睡眠 |
| levels.summary.rem.minutes | REM睡眠 |
Step10 MySQLにデータを入れてみる
1.キーとのマッピング情報
day:.dateOfSleep =日付単位集計
log_id:.logId =ログの識別
is_main_sleep:.isMainSleep =1日1行判定
fitbit_user_id:user_id =トークン取得時のもの
2.MySQLにINSERTする
INSERT INTO sleep_daily (
day,
fitbit_user_id,
log_id,
is_main_sleep,
deep_minutes,
deep_count,
light_minutes,
light_count,
rem_minutes,
rem_count,
wake_minutes,
wake_count,
updated_at
)
VALUES (
'2026-02-15',
'ユーザーID',
ログID(日毎に変わる),
TRUE,
69,
12,
273,
31,
133,
17,
44,
23,
NOW()
)
ON DUPLICATE KEY UPDATE
log_id = VALUES(log_id),
deep_minutes = VALUES(deep_minutes),
deep_count = VALUES(deep_count),
light_minutes = VALUES(light_minutes),
light_count = VALUES(light_count),
rem_minutes = VALUES(rem_minutes),
rem_count = VALUES(rem_count),
wake_minutes = VALUES(wake_minutes),
wake_count = VALUES(wake_count),
updated_at = NOW();
※メモ
INSERT INTOでテーブルに値を読み込み、
VALUEでカラム順と完全一致で数値を入力する
ON DUPLICATE KEY UPDATE :同じ日付を再読み込みした際には更新とする
Step11 Grafanaで可視化
今後、試したい事
・API取得の自動化
・CI/CD
・IaC化
