Pinia 可観測性の作り方:DevTools × OpenTelemetry で変更を可視化する

Vue.js や Nuxt.js でグローバルな状態管理を行う際、Pinia は非常に人気のあるライブラリです。しかし、アプリケーションが大規模化するにつれて「いつ、どこで、どのような状態変更が起きたのか」を追跡することが難しくなります。そこで注目したいのが、DevTools と OpenTelemetry を組み合わせた可観測性の実装です。この記事では、Pinia の状態変更を可視化し、パフォーマンスやデバッグを劇的に改善する方法をご紹介します。
背景
Pinia は Vue 3 の Composition API に完全対応した次世代の状態管理ライブラリとして、Vuex の後継として広く採用されています。シンプルな API と TypeScript の強力なサポートにより、開発体験が大幅に向上しました。
しかし、実運用では以下のような課題に直面することが少なくありません。
Pinia の基本構造
Pinia は Store という単位で状態を管理し、各 Store は以下の要素で構成されます。
- State: アプリケーションの状態を保持
- Getters: 状態から派生した値を計算
- Actions: 状態を変更するメソッド
以下の図は、Pinia の基本的なアーキテクチャを示しています。
mermaidflowchart TB
component["Vue コンポーネント"]
store["Pinia Store"]
state["State<br/>(状態)"]
getters["Getters<br/>(算出値)"]
actions["Actions<br/>(変更処理)"]
component -->|状態参照| state
component -->|算出値参照| getters
component -->|アクション呼び出し| actions
actions -->|状態更新| state
state -->|依存| getters
getters -->|リアクティブ| component
この構造により、Vue コンポーネントから直接 Store の状態にアクセスし、Actions を通じて状態を更新できます。
なぜ可観測性が必要なのか
アプリケーションが成長するにつれて、以下のような問題が顕在化します。
- 複数の Store が相互に依存し、状態変更の流れが追いづらい
- パフォーマンスのボトルネックがどこにあるのか特定できない
- バグ発生時に、どの Action がどのタイミングで呼ばれたのか不明
- ユーザー操作と状態変更の因果関係が見えにくい
こうした課題を解決するために、可観測性(Observability) の概念が重要になってきます。
課題
Pinia を使った開発では、以下のような具体的な課題が発生します。
1. 状態変更の追跡が困難
複数のコンポーネントから同じ Store にアクセスすると、どのコンポーネントがいつ状態を変更したのかを把握するのが難しくなります。
typescript// userStore.ts
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
age: 0,
isLoggedIn: false,
}),
actions: {
updateUser(name: string, age: number) {
this.name = name;
this.age = age;
},
login() {
this.isLoggedIn = true;
},
},
});
上記のような Store があった場合、複数のコンポーネントから updateUser
が呼ばれると、変更の追跡が困難です。
2. パフォーマンスボトルネックの特定
どの Getter や Action が処理時間を消費しているのか、標準の Pinia だけでは測定できません。
typescript// productStore.ts
import { defineStore } from 'pinia';
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
}),
getters: {
// この Getter が重い処理かもしれない
expensiveCalculation: (state) => {
return state.products.map((p) => {
// 複雑な計算処理
return heavyComputation(p);
});
},
},
});
3. デバッグ時の情報不足
本番環境でエラーが発生した際、「どの順番で Action が実行されたか」「そのときの State はどうだったか」といった情報が不足しがちです。
以下の図は、可観測性がない場合の課題を示しています。
mermaidflowchart LR
user["ユーザー操作"]
comp1["コンポーネント A"]
comp2["コンポーネント B"]
comp3["コンポーネント C"]
store["Pinia Store"]
unknown["❓<br/>どこで変更?<br/>いつ変更?<br/>なぜ変更?"]
user --> comp1
user --> comp2
user --> comp3
comp1 -.->|状態変更| store
comp2 -.->|状態変更| store
comp3 -.->|状態変更| store
store --> unknown
このように、状態変更の発生源や理由が不透明になってしまいます。
解決策
これらの課題を解決するために、DevTools と OpenTelemetry を組み合わせた可観測性の仕組みを実装します。
DevTools による開発時の可視化
Vue DevTools には Pinia 専用のタブが用意されており、以下の情報を確認できます。
- 現在の Store の状態
- Actions の実行履歴
- State の変更タイムライン
これにより、開発時のデバッグが格段に楽になります。
OpenTelemetry による本番環境の監視
OpenTelemetry は、分散トレーシング・メトリクス・ログを統合的に扱うための標準規格です。Pinia と組み合わせることで、以下が実現できます。
- トレース: Action の実行時間や呼び出し元を記録
- メトリクス: 状態変更の頻度や Getter の計算時間を測定
- ログ: 状態変更の詳細情報を記録
以下の図は、DevTools と OpenTelemetry を組み合わせた可観測性アーキテクチャを示しています。
mermaidflowchart TB
user["ユーザー操作"]
component["Vue コンポーネント"]
pinia["Pinia Store"]
plugin["Pinia プラグイン<br/>(計装層)"]
devtools["Vue DevTools<br/>(開発環境)"]
otel["OpenTelemetry<br/>(本番環境)"]
backend["バックエンド<br/>(Jaeger/Prometheus)"]
user --> component
component --> pinia
pinia --> plugin
plugin --> devtools
plugin --> otel
otel --> backend
style plugin fill:#e1f5ff
style devtools fill:#fff3cd
style otel fill:#d1ecf1
このアーキテクチャにより、開発環境では DevTools で即座にデバッグし、本番環境では OpenTelemetry で継続的に監視できます。
実装の基本方針
- Pinia プラグインの作成: 状態変更を検知し、計装する
- DevTools との連携: 開発時の可視化を強化
- OpenTelemetry の統合: トレースとメトリクスを記録
- バックエンドへの送信: Jaeger や Prometheus で分析
具体例
それでは、実際のコードを見ながら実装方法を解説していきます。
プロジェクトのセットアップ
まず、必要なパッケージをインストールします。
bashyarn add pinia
yarn add @opentelemetry/api @opentelemetry/sdk-trace-web @opentelemetry/exporter-trace-otlp-http
yarn add --dev @vue/devtools-api
これらのパッケージにより、Pinia、OpenTelemetry、DevTools API が利用可能になります。
Pinia プラグインの作成
Pinia プラグインを作成し、Action の実行を監視します。
typescript// plugins/piniaObservability.ts
import { PiniaPluginContext } from 'pinia';
import { trace, Span } from '@opentelemetry/api';
// OpenTelemetry のトレーサーを取得
const tracer = trace.getTracer('pinia-store', '1.0.0');
export function piniaObservabilityPlugin(
context: PiniaPluginContext
) {
const { store, options } = context;
// Store の名前を取得
const storeName = options.id || 'unknown-store';
return {};
}
まず、OpenTelemetry の tracer
を初期化し、Store 名を取得します。
Action の計装
次に、各 Action の実行をトレースします。
typescript// plugins/piniaObservability.ts (続き)
export function piniaObservabilityPlugin(
context: PiniaPluginContext
) {
const { store, options } = context;
const storeName = options.id || 'unknown-store';
// 元の Actions を保存
const originalActions = { ...store.$actions };
// 各 Action をラップ
Object.keys(originalActions).forEach((actionName) => {
const originalAction = store[actionName];
// Action をトレース付きでラップ
store[actionName] = function (...args: any[]) {
// トレースのスパンを開始
const span = tracer.startSpan(
`${storeName}.${actionName}`
);
try {
// 元の Action を実行
const result = originalAction.apply(this, args);
// Promise の場合は完了を待つ
if (result instanceof Promise) {
return result.finally(() => span.end());
}
// 同期処理の場合は即座に終了
span.end();
return result;
} catch (error) {
// エラーを記録
span.recordException(error as Error);
span.end();
throw error;
}
};
});
}
このコードにより、すべての Action が OpenTelemetry のスパンとして記録されます。
State の変更を監視
State の変更を検知し、メトリクスとして記録します。
typescript// plugins/piniaObservability.ts (続き)
import { watch } from 'vue';
export function piniaObservabilityPlugin(
context: PiniaPluginContext
) {
const { store, options } = context;
const storeName = options.id || 'unknown-store';
// ... (Action の計装コードは省略) ...
// State の変更を監視
watch(
() => store.$state,
(newState, oldState) => {
// 変更された項目を検出
const changes = detectChanges(oldState, newState);
// 各変更をログに記録
changes.forEach((change) => {
console.log(
`[${storeName}] ${change.path}: ${change.oldValue} → ${change.newValue}`
);
// OpenTelemetry でメトリクスとして記録
const meter = metrics.getMeter('pinia-store');
const counter = meter.createCounter(
`${storeName}.state.changes`
);
counter.add(1, { path: change.path });
});
},
{ deep: true }
);
}
これにより、State のどのプロパティがいつ変更されたかを追跡できます。
typescript// plugins/piniaObservability.ts - 変更検出のヘルパー関数
function detectChanges(
oldState: any,
newState: any,
path = ''
): Array<{ path: string; oldValue: any; newValue: any }> {
const changes: Array<{
path: string;
oldValue: any;
newValue: any;
}> = [];
// すべてのキーを取得
const keys = new Set([
...Object.keys(oldState || {}),
...Object.keys(newState || {}),
]);
keys.forEach((key) => {
const currentPath = path ? `${path}.${key}` : key;
const oldValue = oldState?.[key];
const newValue = newState?.[key];
// オブジェクトの場合は再帰的に比較
if (
typeof oldValue === 'object' &&
typeof newValue === 'object' &&
oldValue !== null &&
newValue !== null
) {
changes.push(
...detectChanges(oldValue, newValue, currentPath)
);
} else if (oldValue !== newValue) {
changes.push({
path: currentPath,
oldValue,
newValue,
});
}
});
return changes;
}
OpenTelemetry の初期化
アプリケーション起動時に OpenTelemetry を初期化します。
typescript// plugins/telemetry.ts
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
export function initTelemetry() {
// トレースプロバイダーを作成
const provider = new WebTracerProvider({
resource: {
attributes: {
'service.name': 'my-vue-app',
'service.version': '1.0.0',
},
},
});
// OTLP エクスポーターを設定
const exporter = new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces', // バックエンドの URL
});
// スパンプロセッサーを追加
provider.addSpanProcessor(
new SimpleSpanProcessor(exporter)
);
// グローバルに登録
provider.register();
}
この設定により、トレースデータが指定したバックエンドに送信されます。
Nuxt.js での統合
Nuxt.js プロジェクトで Pinia プラグインを登録します。
typescript// plugins/pinia.ts
import { defineNuxtPlugin } from '#app';
import { piniaObservabilityPlugin } from './piniaObservability';
import { initTelemetry } from './telemetry';
export default defineNuxtPlugin((nuxtApp) => {
// OpenTelemetry を初期化
if (process.client) {
initTelemetry();
}
// Pinia インスタンスを取得
const pinia = nuxtApp.$pinia;
// プラグインを登録
if (pinia) {
pinia.use(piniaObservabilityPlugin);
}
});
クライアントサイドでのみ OpenTelemetry を初期化し、Pinia にプラグインを追加します。
実際の Store での利用
作成したプラグインは、すべての Store に自動的に適用されます。
typescript// stores/cart.ts
import { defineStore } from 'pinia';
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as Array<{
id: string;
name: string;
price: number;
}>,
total: 0,
}),
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) =>
state.items.reduce(
(sum, item) => sum + item.price,
0
),
},
actions: {
// この Action は自動的にトレースされる
addItem(item: {
id: string;
name: string;
price: number;
}) {
this.items.push(item);
this.total = this.totalPrice;
},
// この Action も同様にトレースされる
removeItem(itemId: string) {
const index = this.items.findIndex(
(item) => item.id === itemId
);
if (index !== -1) {
this.items.splice(index, 1);
this.total = this.totalPrice;
}
},
},
});
プラグインにより、addItem
や removeItem
が実行されると、自動的にトレースが記録されます。
DevTools での確認
Vue DevTools を開くと、Pinia タブで以下の情報を確認できます。
# | 項目 | 説明 |
---|---|---|
1 | Store 一覧 | 登録されているすべての Store を表示 |
2 | State の現在値 | 各 Store の State をリアルタイムで表示 |
3 | Actions の実行履歴 | どの Action がいつ実行されたかを記録 |
4 | State の変更履歴 | State がどのように変化したかを追跡 |
5 | Time Travel デバッグ | 過去の State に戻って動作を確認できる機能 |
以下の図は、DevTools と OpenTelemetry によるトレースフローを示しています。
mermaidsequenceDiagram
participant User as ユーザー
participant Comp as コンポーネント
participant Store as Pinia Store
participant Plugin as プラグイン
participant Dev as DevTools
participant OTel as OpenTelemetry
User->>Comp: ボタンクリック
Comp->>Store: addItem() 呼び出し
Store->>Plugin: Action 実行前フック
Plugin->>OTel: スパン開始
Plugin->>Dev: Action 記録
Store->>Store: State 更新
Store->>Plugin: State 変更検知
Plugin->>OTel: メトリクス記録
Plugin->>Dev: State 変更通知
Plugin->>OTel: スパン終了
OTel-->>User: トレース送信 (バックエンド)
Dev-->>User: DevTools 更新
このシーケンス図により、ユーザー操作から State 変更、トレース記録までの一連の流れが理解できます。
Jaeger でのトレース確認
バックエンドに Jaeger を使用している場合、以下のような情報を確認できます。
# | 項目 | 説明 |
---|---|---|
1 | トレース ID | 一連の操作を識別する一意の ID |
2 | スパンの階層構造 | どの Action がどの順番で実行されたか |
3 | 実行時間 | 各 Action の処理時間 |
4 | エラー情報 | 例外が発生した場合のスタックトレース |
5 | カスタム属性 | Store 名や引数などの追加情報 |
これにより、本番環境でのパフォーマンス問題やエラーを詳細に分析できます。
パフォーマンス測定の例
Getter の計算時間を測定する場合は、以下のように拡張できます。
typescript// plugins/piniaObservability.ts - Getter の計装
export function piniaObservabilityPlugin(
context: PiniaPluginContext
) {
const { store, options } = context;
const storeName = options.id || 'unknown-store';
// Getters を計装
if (options.getters) {
Object.keys(options.getters).forEach((getterName) => {
const originalGetter = store[getterName];
Object.defineProperty(store, getterName, {
get() {
const startTime = performance.now();
const result = originalGetter;
const duration = performance.now() - startTime;
// 実行時間をメトリクスとして記録
console.log(
`[${storeName}] Getter "${getterName}" took ${duration.toFixed(
2
)}ms`
);
return result;
},
});
});
}
}
これにより、重い Getter を特定し、最適化の対象を明確にできます。
エラートラッキングの強化
Action 内でエラーが発生した場合、詳細な情報を記録します。
typescript// stores/user.ts - エラーハンドリング付き Action
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null as User | null,
loading: false,
error: null as string | null,
}),
actions: {
async fetchUser(userId: string) {
this.loading = true;
this.error = null;
try {
const response = await fetch(
`/api/users/${userId}`
);
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}`
);
}
this.user = await response.json();
} catch (error) {
// エラーは自動的にトレースに記録される
this.error = error.message;
throw error;
} finally {
this.loading = false;
}
},
},
});
プラグインにより、エラーが OpenTelemetry のスパンに記録され、バックエンドで分析できます。
まとめ
この記事では、Pinia の可観測性を実現するために DevTools と OpenTelemetry を組み合わせる方法を解説しました。
実現できたこと
# | 項目 | 効果 |
---|---|---|
1 | 開発時の可視化 | DevTools で State 変更と Action をリアルタイム追跡 |
2 | 本番環境の監視 | OpenTelemetry でトレースとメトリクスを記録 |
3 | パフォーマンス測定 | Action や Getter の実行時間を定量的に把握 |
4 | エラートラッキング | エラー発生時の詳細な情報を記録 |
5 | デバッグの効率化 | Time Travel デバッグで過去の State を再現 |
導入のメリット
可観測性を導入することで、以下のようなメリットが得られます。
- 問題の早期発見: パフォーマンス劣化やエラーを即座に検知できます
- デバッグ時間の短縮: 状態変更の流れが明確になり、原因特定が容易になります
- チーム開発の効率化: 状態管理の動作が可視化され、レビューが円滑になります
- 本番環境の安心感: リアルタイムで監視でき、問題発生時に迅速に対応できます
次のステップ
さらに高度な可観測性を実現するには、以下のような拡張も検討できます。
- カスタムメトリクスの追加: ビジネスロジック固有の指標を記録
- アラートの設定: 異常な状態変更を検知して通知
- ダッシュボードの構築: Grafana などで可視化
- 分散トレーシング: API 呼び出しとの連携を追跡
Pinia の可観測性を実装することで、より堅牢で保守性の高いアプリケーションを構築できます。ぜひ、あなたのプロジェクトにも導入してみてください。
関連リンク
- article
Pinia 可観測性の作り方:DevTools × OpenTelemetry で変更を可視化する
- article
Pinia 正規化データ設計:Entity アダプタで巨大リストを高速・一貫に保つ
- article
Pinia ストア分割テンプレ集:domain/ui/session の三層パターン
- article
Pinia をフレームワークレスで SSR:Nitro/Express 直結の同形レンダリング
- article
Pinia と TanStack Query の使い分けを徹底検証:サーバー/クライアント状態の最適解
- article
Pinia で状態が更新されない?参照の再利用・シャロー比較・getter 依存の落とし穴
- article
Flutter とは?2025 年版:仕組み・強み・向いているプロダクトを徹底解説
- article
Pinia 可観測性の作り方:DevTools × OpenTelemetry で変更を可視化する
- article
Obsidian 日次・週次レビュー運用:テンプレ+ Dataview で継続する仕組み
- article
フィーチャーフラグ運用:Zustand で段階的リリースとリモート設定を実装
- article
Nuxt 本番運用チェックリスト:セキュリティヘッダー・CSP・Cookie 設定を総点検
- article
WordPress 技術アーキテクチャ図解:フック/ループ/クエリの全体像を一枚で理解
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来