モバイルアプリバックエンドの可観測性を実装する:ツール比較と実践ガイド
モバイルアプリバックエンドの可観測性を実装する:ツール比較と実践ガイド
こんにちは。
今日は、多くのモバイルアプリに携わるソフトウェアエンジニアが直面する課題
「バックエンドからの応答が遅かったり止まった時に、裏で何が起きているのかわからない」
という問題を解決する方法について、一緒に考えてみましょう。
可観測性とは何か
可観測性(Observability)という言葉を聞いたことがありますか?
これはシステムの 外部出力(ログ、メトリクス、トレース)から内部状態を推測できる度合い を示す概念です。
制御理論の世界から来た言葉ですが、要するに「システムが『何をしているか』『なぜそうしているか』『今どんな状態か』が、データから理解できる状態」という意味です。
モニタリング vs 可観測性
「モニタリングと何が違うの?」という疑問が出そうですね。
- モニタリング:既知の問題に対して「これが起きているか?」と監視する
- 可観測性:未知の問題に対して「何が起きているか?」を探求する
モバイルアプリのバックエンド環境では、毎日予想外のユーザー行動やトラフィックパターンが発生します。事前に全ての問題を定義することは不可能です。だからこそ、可観測性が必要なのです。
可観測性の3本柱
可観測性は3つの要素から成り立っています:
1. ログ(Logs)
個別のイベントの詳細な記録です。「ユーザーAが時刻Bに操作Cを実行した」といった、一回限りの出来事を記録します。
[2025-11-13 15:30:45] INFO User login_success user_id=12345 region=JP
[2025-11-13 15:30:46] DEBUG Database query_executed query_type=SELECT duration_ms=142
[2025-11-13 15:30:47] ERROR API call failed endpoint=/api/profile http_status=500
2. メトリクス(Metrics)
時系列で変化する数値データです。CPU使用率、API応答時間、リクエスト数などが該当します。
api.response.time: 145ms (現在)
db.connection.pool.active: 23/30 (現在)
http.request.rate: 1250/min (現在)
3. トレース(Traces)
1つのリクエストがシステム全体を通じてどう流れるかを追跡します。モバイルアプリからの1つのリクエストが、Front Door → APIM → Container Apps → Database → Blob Storage まで、どこでどのくらい時間がかかったかを記録します。
Request ID: abc-def-123
├── Front Door: 5ms
├── APIM: 8ms
├── Container Apps: 120ms
│ ├── Authentication: 15ms
│ ├── Business Logic: 90ms
│ └── Database Query: 25ms
├── Blob Storage: 12ms
└── Total: 145ms
モバイルアプリバックエンド:代表的なアーキテクチャ
可観測性を語る前に、わたしたちが監視するシステムはどんな構成でしょうか。モバイルアプリのバックエンド例を見てみましょう。
┌─────────────────────┐
│ モバイルアプリ │
│ (Firebase Analytics)│
└──────────┬──────────┘
│
HTTPS/gRPC
│
┌──────▼──────┐
│ Front Door │ (DDoS対策、キャッシング)
└──────┬──────┘
│
┌──────▼──────┐
│ APIM │ (認証、レート制限、バージョン管理)
└──────┬──────┘
│
┌──────▼──────────────────────┐
│ Container Apps × N │
│ (マイクロサービス群) │
└──────┬──────┬──────┬────────┘
│ │ │
┌──────▼──┐┌──▼───┐┌─▼───────┐
│ DB ││Blob ││Event │
│Service ││Stor ││Grid │
└─────────┘└──────┘└─────────┘
ここに 可観測性を組み込まないと、問題発生時に対応チームはブラックボックスの中を手探りしることになります。
ツール比較:Zabbix・Grafana・Datadog・New Relic・PagerDuty
では、どのツールを選ぶべきか。それぞれの特性を比較してみましょう。
Zabbix(無料・オンプレミス向け)
向いている用途:既存のオンプレミスシステムがある場合、安価に実装したい場合
| 項目 | Zabbix |
|---|---|
| コスト | 無料(オープンソース) |
| セットアップ | 中程度(自分でサーバを構築) |
| スケーラビリティ | 大規模環境では工夫が必要 |
| ログ・トレース | 弱い(メトリクス中心) |
| アラート | あり(メール、Slack等) |
| クラウドネイティブ対応 | 低い |
実例:社内のレガシーシステムも監視したいなら選択肢に
Grafana(可視化特化)
向いている用途:既存メトリクス集約ツールの可視化層が欲しい場合
| 項目 | Grafana |
|---|---|
| コスト | 無料 + ホスティング料金 |
| セットアップ | 簡単(ダッシュボード作成が楽) |
| スケーラビリティ | 高い(Prometheus等のバックエンドに依存) |
| ログ・トレース | Loki, Tempo統合で対応 |
| アラート | あり(柔軟) |
| クラウドネイティブ対応 | 高い |
実例:Prometheus + Grafana + Loki + Tempoの組み合わせで、可観測性3本柱を全てカバー
Datadog(SaaS・オールインワン)
向いている用途:手間をかけず、ログ・メトリクス・トレースを一元管理したい場合
| 項目 | Datadog |
|---|---|
| コスト | 従量課金(ホストあたり月数千〜数万円) |
| セットアップ | 簡単(エージェント導入 + 設定) |
| スケーラビリティ | 非常に高い(SaaS) |
| ログ・トレース | 全て標準装備 |
| アラート | 非常に高度(AI異常検知も含む) |
| クラウドネイティブ対応 | 最高レベル |
実例:モバイルアプリのバックエンドが急速に成長する企業向け。スケーラビリティの心配が不要
New Relic(開発者向けSaaS)
向いている用途:アプリケーションパフォーマンス重視、開発チームが主体
| 項目 | New Relic |
|---|---|
| コスト | 従量課金(Datadogより若干安い傾向) |
| セットアップ | 簡単(APM標準装備) |
| スケーラビリティ | 高い(SaaS) |
| ログ・トレース | 全て標準装備 |
| アラート | あり(APM中心) |
| クラウドネイティブ対応 | 高い |
実例:JavaScriptやPythonなど、特定の言語エコシステムに深い統合が必要な場合
PagerDuty(インシデント管理)
向いている用途:アラート→対応の自動化、オンコール管理
| 項目 | PagerDuty |
|---|---|
| コスト | ユーザーあたり月1,000〜3,000円程度 |
| セットアップ | 簡単(他ツールとの統合中心) |
| スケーラビリティ | 高い(SaaS) |
| ログ・トレース | 無し(インシデント管理に特化) |
| アラート | アラート+エスカレーション+通知 |
| クラウドネイティブ対応 | 高い(Slack, Teams, SMS等と統合) |
重要な注意:PagerDutyはアラート 通知・管理 ツールであり、データ収集ツールではありません。Datadog/New Relic等と組み合わせて使います。
ツール選択の実践的な考え方
正直に言うと「このツールが最高」という答えはありません。選択は 組織の成熟度 によって変わります。
段階1:スタートアップ(予算が限られている)
→ Grafana (無料) + Prometheus + Loki + Tempoで自前構築。AzureならApplication Insightsも活用
段階2:成長期(チーム5名以上、月額1万ドル以下の予算)
→ Datadog。スケーラビリティの心配が減り、エンジニアの手間が大幅削減される
段階3:スケール期(複数地域、複数チーム)
→ Datadog + PagerDuty。インシデント対応の自動化が重要に
実践例:モバイルアプリバックエンドの可観測性をCDKTFで実装
では、具体的にどう実装するか。Azureのサービスとcdktfを使った例を見てみましょう。
ステップ1:基本的なアーキテクチャ
// cdktf.json
{
"language": "typescript",
"terraformVersion": ">= 1.0",
"backends": {
"azurerm": {
"resource_group_name": "rg-tfstate",
"storage_account_name": "sttfstate",
"container_name": "tfstate"
}
}
}
ステップ2:Container Apps + Application Insightsの基本設定
import { Construct } from "constructs";
import { TerraformStack } from "cdktf";
import * as azurerm from "@cdktf/provider-azurerm";
interface ObservabilityConfig {
environment: string;
location: string;
resourceGroupName: string;
}
export class MobileBackendObservabilityStack extends TerraformStack {
constructor(scope: Construct, id: string, config: ObservabilityConfig) {
super(scope, id);
new azurerm.provider.AzurermProvider(this, "azure", {
features: {},
skipProviderRegistration: false,
});
const rg = new azurerm.resourceGroup.ResourceGroup(
this,
"rg-mobile-backend",
{
name: config.resourceGroupName,
location: config.location,
}
);
// Application Insights(可観測性の基盤)
const appInsights = new azurerm.applicationInsights.ApplicationInsights(
this,
"app-insights-mobile",
{
name: `appinsights-mobile-${config.environment}`,
location: config.location,
resourceGroupName: rg.name,
applicationType: "web",
retentionInDays: 90,
tags: {
environment: config.environment,
observability: "true",
},
}
);
// Log Analytics Workspace(ログ集約の中心)
const workspace = new azurerm.logAnalyticsWorkspace.LogAnalyticsWorkspace(
this,
"laws-mobile",
{
name: `laws-mobile-${config.environment}`,
location: config.location,
resourceGroupName: rg.name,
skuName: "PerGB2018",
retentionInDays: 30,
tags: {
environment: config.environment,
},
}
);
// Container Registry
const acr = new azurerm.containerRegistry.ContainerRegistry(
this,
"acr-mobile",
{
name: `acrmobile${config.environment}`,
resourceGroupName: rg.name,
location: config.location,
skuName: "Standard",
adminEnabled: true,
tags: {
environment: config.environment,
},
}
);
// Container Apps Environment
const containerAppEnv =
new azurerm.containerAppEnvironment.ContainerAppEnvironment(
this,
"cae-mobile",
{
name: `cae-mobile-${config.environment}`,
location: config.location,
resourceGroupName: rg.name,
infrastructureResourceGroupName: `rg-cae-infra-${config.environment}`,
logAnalyticsWorkspaceId: workspace.id,
tags: {
environment: config.environment,
observability: "true",
},
}
);
// Database(SQL Server + SQL Database)
const sqlServer = new azurerm.mssqlServer.MssqlServer(
this,
"sqlserver-mobile",
{
name: `sqlserver-mobile-${config.environment}`,
location: config.location,
resourceGroupName: rg.name,
version: "12.0",
administratorLogin: "sqladmin",
administratorLoginPassword: "P@ssw0rd2024!", // 本番環境ではSecrets Vaultから
tags: {
environment: config.environment,
},
}
);
const sqlDb = new azurerm.mssqlDatabase.MssqlDatabase(this, "sqldb-app", {
name: "db-mobile-app",
serverId: sqlServer.id,
skuName: "Standard",
tags: {
environment: config.environment,
},
});
// Diagnostic Settings: SQL Server → Log Analytics
new azurerm.monitorDiagnosticSetting.MonitorDiagnosticSetting(
this,
"diag-sql",
{
name: "diag-sql-server",
targetResourceId: sqlServer.id,
logAnalyticsWorkspaceId: workspace.id,
enabledLog: [
{
category: "SQLSecurityAuditEvents",
enabled: true,
},
{
category: "Errors",
enabled: true,
},
],
metric: [
{
category: "Basic",
enabled: true,
},
],
}
);
// Blob Storage
const storageAccount = new azurerm.storageAccount.StorageAccount(
this,
"st-mobile-assets",
{
name: `stmobile${config.environment}`,
resourceGroupName: rg.name,
location: config.location,
accountTier: "Standard",
accountReplicationType: "GRS",
tags: {
environment: config.environment,
},
}
);
// Blob Container
new azurerm.storageContainer.StorageContainer(
this,
"blob-container-images",
{
name: "user-images",
storageAccountName: storageAccount.name,
containerAccessType: "private",
}
);
// Front Door
const frontDoor = new azurerm.cdnFrontdoorProfile.CdnFrontdoorProfile(
this,
"fd-mobile",
{
name: `fd-mobile-${config.environment}`,
resourceGroupName: rg.name,
skuName: "Standard_AzureFrontDoor",
tags: {
environment: config.environment,
observability: "true",
},
}
);
// APIM(API Management)
const apim = new azurerm.apiManagementService.ApiManagementService(
this,
"apim-mobile",
{
name: `apim-mobile-${config.environment}`,
location: config.location,
resourceGroupName: rg.name,
publisherName: "Mobile Platform Team",
publisherEmail: "api@example.com",
skuName: "Developer_1",
tags: {
environment: config.environment,
observability: "true",
},
}
);
// Event Grid Topic(ロギングとアナリティクス用)
const eventGridTopic = new azurerm.eventgridTopic.EventgridTopic(
this,
"eg-mobile-events",
{
name: `eg-mobile-${config.environment}`,
location: config.location,
resourceGroupName: rg.name,
tags: {
environment: config.environment,
},
}
);
return {
appInsights,
workspace,
containerAppEnv,
sqlServer,
sqlDb,
storageAccount,
frontDoor,
apim,
eventGridTopic,
rg,
};
}
}
ステップ3:アプリケーション内のトレース実装(Node.js例)
// src/observability/tracing.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { AzureMonitorTraceExporter } from "@azure/monitor-opentelemetry-exporter";
import { Resource } from "@opentelemetry/resources";
import { SemanticResourceAttributes } from "@opentelemetry/semantic-conventions";
// Application Insightsへのトレース送信
export function initializeTracing(instrumentationKey: string) {
const resource = Resource.default().merge(
new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: "mobile-backend-api",
[SemanticResourceAttributes.SERVICE_VERSION]: "1.0.0",
environment: process.env.ENVIRONMENT || "development",
})
);
const traceExporter = new AzureMonitorTraceExporter({
connectionString: `InstrumentationKey=${instrumentationKey}`,
});
const sdk = new NodeSDK({
resource: resource,
traceExporter: traceExporter,
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
console.log("📊 Tracing initialized with Application Insights");
return sdk;
}
ステップ4:Express内でのカスタムトレーシング
// src/middleware/observability.middleware.ts
import { Request, Response, NextFunction } from "express";
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("mobile-backend");
export function observabilityMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const span = tracer.startSpan(`HTTP ${req.method} ${req.path}`);
// リクエスト情報をspan属性に記録
span.setAttributes({
"http.method": req.method,
"http.url": req.originalUrl,
"http.client_ip": req.ip,
"http.user_agent": req.get("user-agent"),
"http.request_id": req.id || generateRequestId(),
});
// レスポンス監視
res.on("finish", () => {
span.setAttributes({
"http.status_code": res.statusCode,
"http.response_time_ms": Date.now(),
});
if (res.statusCode >= 400) {
span.addEvent("error", {
"error.status": res.statusCode,
});
}
span.end();
});
next();
}
function generateRequestId(): string {
return `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
ステップ5:Firebase Analytics統合
// src/analytics/firebase-integration.ts
import * as admin from "firebase-admin";
export class FirebaseAnalyticsIntegration {
private analytics: admin.analytics.Analytics;
constructor() {
admin.initializeApp();
this.analytics = admin.analytics();
}
async logMobileEvent(userId: string, eventName: string, data: any) {
// FirebaseからのイベントをLog Analyticsに転送
console.log(`[MOBILE_EVENT] User: ${userId}, Event: ${eventName}`, data);
// イベントのメタデータ
const event = {
timestamp: new Date().toISOString(),
userId,
eventName,
properties: data,
source: "firebase",
};
// Event GridへのPublish(ロギング用)
// await this.publishToEventGrid(event);
}
async getUserJourney(userId: string) {
// Firebase Analyticsからユーザーの行動データを取得
// このデータはLog Analyticsと組み合わせて、
// バックエンド+フロントエンドの統合ビューを作成できます
}
}
ステップ6:Kusto Query Language(KQL)でのログ分析
Application Insightsの背後にあるLog Analyticsを使ったクエリ例:
// 過去1時間のAPI応答時間の分布
requests
| where timestamp > ago(1h)
| summarize
AvgDuration=avg(duration),
P95Duration=percentile(duration, 95),
P99Duration=percentile(duration, 99),
Count=count()
by name, resultCode
| order by Count desc
// Container Appsのエラーレート監視
containerAppConsoleLogs_CL
| where TimeGenerated > ago(5m)
| where log_level_s == "ERROR"
| summarize ErrorCount=count() by container_name_s, pod_name_s
| where ErrorCount > 10
// ユーザーの平均セッション時間(Firebase + Application Insights)
customEvents
| where name == "user_session_end"
| extend sessionDuration=toint(customDimensions.duration_ms)
| summarize AvgSessionDuration=avg(sessionDuration), UserCount=dcount(user_Id)
by bin(timestamp, 1h)
CDKTFでの可観測性設定のベストプラクティス
1. Diagnostic Settings の一元管理
全てのAzureリソースのログを自動的にLog Analyticsに送信する仕組みを作ります:
// utils/diagnostic-settings.ts
export function createDiagnosticSettings(
scope: Construct,
resourceId: string,
workspaceId: string,
resourceName: string
) {
return new azurerm.monitorDiagnosticSetting.MonitorDiagnosticSetting(
scope,
`diag-${resourceName}`,
{
name: `diagnostic-${resourceName}`,
targetResourceId: resourceId,
logAnalyticsWorkspaceId: workspaceId,
enabledLog: [
{
category: "Administrative",
enabled: true,
},
{
category: "Security",
enabled: true,
},
{
category: "ServiceHealth",
enabled: true,
},
],
metric: [
{
category: "AllMetrics",
enabled: true,
retentionPolicy: {
days: 30,
enabled: true,
},
},
],
}
);
}
2. アラートルールの定義
// src/alerts/critical-alerts.ts
export function createCriticalAlerts(
scope: Construct,
workspaceId: string,
actionGroupId: string
) {
// API応答時間が高い場合
new azurerm.monitorScheduledQueryRulesAlert.MonitorScheduledQueryRulesAlert(
scope,
"alert-high-response-time",
{
name: "Alert: High API Response Time",
location: "eastasia",
resourceGroupName: rg.name,
scopeId: workspaceId,
description: "API応答時間が500msを超えた場合",
query: `
requests
| where duration > 500
| summarize Count=count() by name
| where Count > 10
`,
severity: 3,
frequencyInMinutes: 5,
timeWindowInMinutes: 5,
triggerOperator: "GreaterThan",
triggerThreshold: 0,
action: {
actionGroupIds: [actionGroupId],
},
}
);
// エラーレートの急上昇
new azurerm.monitorScheduledQueryRulesAlert.MonitorScheduledQueryRulesAlert(
scope,
"alert-error-rate",
{
name: "Alert: High Error Rate",
location: "eastasia",
resourceGroupName: rg.name,
scopeId: workspaceId,
query: `
requests
| where resultCode >= 500
| summarize ErrorRate=(count()*100.0)/sum(1)
| where ErrorRate > 5
`,
severity: 1, // 重大
frequencyInMinutes: 1,
timeWindowInMinutes: 5,
triggerOperator: "GreaterThan",
triggerThreshold: 0,
action: {
actionGroupIds: [actionGroupId],
},
}
);
}
ダッシュボード設計の思想
可観測性がデータ集約だけでは意味がありません。意思決定に繋がるダッシュボード が必要です。
レベル別ダッシュボード設計
1. Executive Dashboard(経営層向け)
- サービス可用性(SLA達成率)
- ユーザー数の推移
- エラーレート
- 平均応答時間
2. Operations Dashboard(運用チーム向け)
- リアルタイムのリクエスト流量
- エラー発生地点(Container Apps、DB、Blob Storage等)
- リソース使用率(CPU、メモリ、接続数)
- アクティブアラート一覧
3. Developer Dashboard(開発チーム向け)
- デプロイメント履歴
- 特定のAPIエンドポイントの詳細分析
- 依存関係の遅延分析(トレース)
- ログレベル別の詳細ログビュー
実装上の注意点
コスト最適化
可観測性ツールは ログの量に応じてコストが増加 します:
- Application Insights:最初は無料、その後100GBまで月100ドル前後
- Datadog:ホストあたり月15ドル + ログ保有量
- Log Analytics:データ取得量に応じた課金
コスト削減のコツ:
- 本番環境では重要なログのみ記録
- ログ保有期間を適切に設定(30-90日が一般的)
- サンプリングを活用(全リクエストではなく、一定割合のみをトレース)
パフォーマンスへの影響
トレーシングライブラリは、アプリケーションのレスポンス時間に影響を与えます。
最小化のコツ:
- 非同期ロギングを使用
- バッチ処理でApplication Insightsに送信
- 本番環境でのサンプリングレート設定(例:10% or 重要リクエストのみ)
// サンプリングの実装例
export const sampler = {
shouldSample: (context: SpanContext) => {
// 本番環境では5%のみサンプリング
if (process.env.ENVIRONMENT === "production") {
return Math.random() < 0.05;
}
// ステージング環境では全てサンプリング
return true;
},
};
PagerDutyの統合例
アラートが発生した時、自動的にオンコール担当者に通知を送ります:
// cdktf設定
const pagerDutyAction = new azurerm.monitorActionGroup.MonitorActionGroup(
scope,
"action-group-pagerduty",
{
name: "PagerDuty Integration",
resourceGroupName: rg.name,
webhookReceiver: [
{
name: "PagerDuty",
// PagerDutyの統合キーに基づくWebhook URL
serviceUri: `https://events.pagerduty.com/v2/enqueue`,
},
],
}
);
まとめ:可観測性は投資である
可観測性の実装には、初期段階で時間とリソースが必要です。でも後から「あの時のエラーはなぜ?」という問題調査に100時間使うより、最初から100倍良い投資です。
モバイルアプリのバックエンドは、ユーザーの直接的な体験に関わります。予測できない問題に素早く対応するために、可観測性は必須です。
選択肢をまとめます:
| 段階 | 推奨ツール | 理由 |
|---|---|---|
| スタートアップ(予算少) | Grafana + Prometheus + Loki | 無料で全機能 |
| スタートアップ(予算中) | Azure Application Insights | Azure統合が深い |
| 成長期以上 | Datadog | スケーラビリティとDX |
| インシデント対応重視 | Datadog + PagerDuty | 自動化と通知 |
ただ
最初から必要以上にリッチな構成で進めようとするブルジョワな偉い人や声の大きい人がいたら、ある意味で赤信号なので要注意です
早めの転職などをオススメしておきます
Discussion