1on1 の「調子どう?」にメトリクスで答える —— Oura Ring × Mackerel で自分を可視化してみた
これは Mackerel Advent Calendar 2025 8日目の記事です。
※本記事はネタ成分が多めです。ちょっとしたコーヒーブレイクのお供に、肩の力を抜いて軽く読んでいただければ幸いです。
はじめに
先日 Mackerel (マカレル)アンバサダーに任命されました!この件に関する思いについては Advent Calendar 1日目の記事に書いていますので、ぜひそちらをご覧ください。
さて、晴れて Mackerel アンバサダーになったからには普段使いレベルで Mackerel を活用し、もっともっと身近な存在にしたい。
そんなことを考え、今回は自分のバイタルデータ(Oura Ring)を Mackerel に送信してダッシュボード化するという試みをやってみました。
なぜこれをやろうと思ったのか
モチベーションとして大きく分けて2つあります。
1. Mackerel を「生活の一部」レベルで使いたい
前述した通りアンバサダーになったので、普段の生活の中で自然に Mackerel に触れる機会をもっと増やしたいと考えました。
通常 Mackerel はサーバーやアプリケーションのパフォーマンスを見るためのものですが、「自分自身」を監視対象に見立てることで、日常的に Mackerel のダッシュボードを確認する習慣がつくと考えました。
2. CTO との 1on1 における「調子どう?」問題の解決
と言いつつ実はこっちがメインの理由なのですが、私は週に 1 回 CTO と 1on1 をやっています。
アジェンダは既に決まっており、冒頭でまず聞かれるのがこのフレーズ。
CTO:「調子どう?」
これ、意外と返答に困りませんか?
「あー、まぁまぁです」「元気です」と感覚値で答えるのは簡単ですが、私はオブザーバビリティを専門とするエンジニアです。
システムの内部状態を語るときには「CPU 使用率が〜」「レイテンシが〜」とメトリクスに基づいた事実ベースで話すのに、自分のコンディションについては「なんとなく元気」という曖昧な主観で報告していいものだろうか??
「自分の調子もメトリクスベースで語るべきだ」
そう思い立ちました。
Oura Ring で計測している睡眠スコアやアクティビティスコアを Mackerel に送り、ダッシュボード化しておけば、1on1 の場でこう言えるはずです。
「ご覧の通り本日の睡眠スコアは85ですので、パフォーマンスは極めて良好です(キリッ」
これがやりたい。ただそれだけのために、私のバイタルデータを Mackerel のダッシュボードで確認できるようにしました。
※冒頭にも書きましたが本記事はネタ成分が多めですので、皆さんあまり真に受けないでくださいね。
今回利用したサービスの紹介
本題に入る前に、今回使用した2つの主要なサービスについて簡単に紹介します。
Mackerel
Mackerel は「はてなブログ」などで有名な「株式会社はてな」が開発・運営している日本製のサーバー監視サービスです。
今年の5月に従来のインフラ監視に加え、アプリケーションのパフォーマンスモニタリング(APM)機能もリリースされ、ますます注目を集めています。
国産なため他の類似サービスとは違い言語の壁が存在しないことが特徴です。母国語で情報収集できることは心理的な負担を大きく軽減してくれます。
また、日本のエンジニアにとって馴染みやすい UI なのも魅力の一つです。Mackerel は必要な機能がギュッと凝縮されており、「多機能すぎない」という価値があります。
Oura Ring
Oura Ring は指輪型の健康管理デバイスです。指にはめるだけで心拍数、心拍変動、体温変化、睡眠の質などを高精度に計測してくれます。スマートウォッチとは異なり、就寝時につけていても邪魔にならないのが特徴です。
実践
それでは、実際にどのようにして Oura Ring のデータを Mackerel に送信したのか、その手順と構成を紹介します。
アーキテクチャ
構築したシステムは AWS を利用した以下のようなサーバーレス構成にしました。
Amazon EventBridge → AWS Lambda → Mackerel
処理の流れとしては以下のとおりです。
- Amazon EventBridge で定期的にイベントを発火(毎朝9時等に設定)
- AWS Lambda が起動し、Oura API を実行してバイタルデータを取得
- 取得したデータをパースし、Mackerel API を使ってデータを Mackerel に送信
Oura API のトークンや Mackerel の API キーといった機密情報は、AWS Secrets Manager で管理し、AWS Lambda から参照できるようにしました。
Oura API のアクセストークンの発行
Oura Cloud API を利用するにはアクセストークンが必要ですが、ここで一つ重要な注意点があります。
以前までは開発者サイトからボタン一つで「Personal Access Tokens」を発行できていたのですが、なんとこれが 2025年末に廃止される というアナウンスが出ています。そのため、今から新規に実装するなら OAuth2 を使う必要があります。
⚠️ Deprecation Warning - Personal Access Tokens ⚠️
Personal Access Tokens will be deprecated by the end of 2025. This is part of an upcoming API platform update and will have no impact on production applications. The Oura API will remain fully available for integration. Please use OAuth2 as outlined in our authentication instructions.
今回は公式ドキュメントに用意されたサンプルコードを利用してトークンを生成しました。
このサンプルの Step 2: Exchange authorization code for access token までを手元の PC で実行しました。ブラウザで認証してコードを取得し、それをスクリプトに渡すことで access_token と refresh_token が生成されます。
ここで取得した refresh_token を AWS Secrets Manager に保存しておき、AWS Lambda 側では「実行のたびに refresh_token を使って新しい access_token を取得する」という実装にしています。
Mackerel API キーの発行
次に、データの送信先となる Mackerel 側の準備です。 API を利用してメトリクスを投稿するには、認証用の API キー が必要になります。
- Mackerel にログイン
- 左上のオーガニゼーション名をクリックし、「概要」タブを選択
- 一覧に表示されている API キーをコピー
もしキーが 1 つもない場合は新しい API キーを作成してください。こちらも Oura API のトークンと同様に AWS Secrets Manager に格納します。
AWS Lambda のサンプルコード
実際に稼働させている Python のコードがこちらです。 ランタイムは Python 3.14 を使用しています。
Python コード
import requests
import os
import json
import boto3
from datetime import datetime, timedelta, timezone
token_url = "https://api.ouraring.com/oauth/token"
secrets_client = boto3.client('secretsmanager')
def get_secret():
"""Get secrets from AWS Secrets Manager."""
secret_name = "oura-metrics-exporter"
try:
response = secrets_client.get_secret_value(SecretId=secret_name)
secret = json.loads(response['SecretString'])
return secret
except Exception as e:
print(f"Error retrieving secret: {e}")
raise e
def update_refresh_token(new_refresh_token):
"""Update refresh token in AWS Secrets Manager."""
secret_name = "oura-metrics-exporter"
try:
current_secrets = get_secret()
current_secrets['refresh_token'] = new_refresh_token
response = secrets_client.update_secret(
SecretId=secret_name,
SecretString=json.dumps(current_secrets)
)
print(f"Successfully updated refresh token in Secrets Manager")
return response
except Exception as e:
print(f"Error updating secret: {e}")
raise e
def refresh_access_token(refresh_token, client_id, client_secret):
"""Refresh the access token using the refresh token."""
token_data = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": client_id,
"client_secret": client_secret
}
response = requests.post(token_url, data=token_data)
new_tokens = response.json()
return new_tokens["access_token"], new_tokens["refresh_token"]
def get_daily_activity_score(access_token, start_date, end_date):
"""Get daily activity score from Oura API."""
url = f"https://api.ouraring.com/v2/usercollection/daily_activity?start_date={start_date}&end_date={end_date}"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)
data = response.json()
if "data" in data and len(data["data"]) > 0:
return data["data"][0].get("score")
return None
def get_daily_sleep_score(access_token, start_date):
"""Get daily sleep score from Oura API."""
url = f"https://api.ouraring.com/v2/usercollection/daily_sleep?start_date={start_date}"
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(url, headers=headers)
data = response.json()
if "data" in data and len(data["data"]) > 0:
return data["data"][0].get("score")
return None
def send_metrics_to_mackerel(mackerel_api_key, activity_score, sleep_score):
"""Send metrics to Mackerel."""
headers = {
"X-Api-Key": mackerel_api_key,
"Content-Type": "application/json"
}
current_time = int(datetime.now(timezone(timedelta(hours=9))).timestamp())
responses = []
if activity_score is not None:
url = "https://api.mackerelio.com/api/v0/services/daily_activity/tsdb"
metrics = [{
"name": "daily_activity_score",
"time": current_time,
"value": activity_score
}]
response = requests.post(url, headers=headers, json=metrics)
responses.append({
"service": "daily_activity",
"status": response.status_code,
"response": response.json()
})
if sleep_score is not None:
url = "https://api.mackerelio.com/api/v0/services/daily_sleep/tsdb"
metrics = [{
"name": "daily_sleep_score",
"time": current_time,
"value": sleep_score
}]
response = requests.post(url, headers=headers, json=metrics)
responses.append({
"service": "daily_sleep",
"status": response.status_code,
"response": response.json()
})
return responses
def lambda_handler(event, context):
try:
secrets = get_secret()
CLIENT_ID = secrets['client_id']
CLIENT_SECRET = secrets['client_secret']
REFRESH_TOKEN = secrets['refresh_token']
MACKEREL_API_KEY = secrets['mackerel_api_key']
print("Refreshing access token...")
access_token, new_refresh_token = refresh_access_token(REFRESH_TOKEN, CLIENT_ID, CLIENT_SECRET)
print(f"\nUpdating refresh token in Secrets Manager...")
update_refresh_token(new_refresh_token)
jst = timezone(timedelta(hours=9))
today = datetime.now(jst).date()
yesterday = today - timedelta(days=1)
day_before_yesterday = today - timedelta(days=2)
start_date = day_before_yesterday.strftime("%Y-%m-%d")
end_date = yesterday.strftime("%Y-%m-%d")
sleep_date = yesterday.strftime("%Y-%m-%d")
print(f"\nFetching daily activity score (from {start_date} to {end_date})...")
activity_score = get_daily_activity_score(access_token, start_date, end_date)
print(f"Daily Activity Score: {activity_score}")
print(f"\nFetching daily sleep score (from {sleep_date})...")
sleep_score = get_daily_sleep_score(access_token, sleep_date)
print(f"Daily Sleep Score: {sleep_score}")
print(f"\nSending metrics to Mackerel...")
mackerel_responses = send_metrics_to_mackerel(
MACKEREL_API_KEY,
activity_score,
sleep_score
)
print(f"Mackerel responses: {mackerel_responses}")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Successfully retrieved Oura metrics and sent to Mackerel',
'activity_score': activity_score,
'sleep_score': sleep_score,
'dates': {
'activity_start': start_date,
'activity_end': end_date,
'sleep_date': sleep_date
},
'mackerel': mackerel_responses
})
}
except Exception as e:
print(f"Error: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps({
'message': 'Error retrieving Oura metrics',
'error': str(e)
})
}
大きく分けて以下の 5 つのステップで動作しています。
- AWS Secrets Manager から Oura API の認証情報と Mackerel API キーを取得
- 取得した
refresh_tokenを使用して Oura API の新しいaccess_tokenとrefresh_tokenを取得 - 次回実行時のために新しい
refresh_tokenを AWS Secrets Manager に登録 -
access_tokenを使って Oura API から「睡眠スコア」と「アクティビティスコア」を取得 - 取得したスコアを Mackerel API 使いサービスメトリックとして送信
完成
メトリクスが Mackerel に送信されたので、ダッシュボードでスコアを確認できるようになりました。

記事公開のギリギリに完成したため、まだメトリクスが溜まっておらずグラフが寂しい感じになってますが、私が Oura Ring で重視しているスコアをダッシュボードに表示できました。
Oura Ring にはこれ以外にも心拍変動や体表温など沢山のメトリクスがあるので、今後はそれらも取り込んでダッシュボードでも相関を見られるようにしていきます。
これから自分のデータが蓄積されていくのが楽しみです。
感想
日々のパフォーマンスを最大化するためには、その資本となる「自身の体」の調子を整えることが何よりも大事だと考えています。
これまでは 1on1 の冒頭で「調子どう?」と聞かれることに備えて、「先週との比較」や「調子の良し悪しの要因」をあらかじめ頭の中で整理してから挑んでいました。
今回作成したダッシュボードを活用すれば準備コストが下がるだけでなく、データに基づいた建設的な会話が可能になります。 「なんとなくダルい」ではなく、「スコアが連続で低下している」といった客観的な事実で語れるようになるため、1on1 の質も向上するはずです。
そして何よりオブザーバビリティを専門にするエンジニアとして、自分自身というシステムの内部状態を可視化し Mackerel のダッシュボードに描画できたことが純粋に嬉しかったです。
皆さんもぜひ、サーバーだけでなく身の回りのあらゆるデータ(室温や体調など)を Mackerel に送信し、オブザーバビリティ・ライフを楽しんでみてはいかがでしょうか?
Discussion