Article

Custom Metricsで業務指標を可視化する方法

高田晃太郎
Custom Metricsで業務指標を可視化する方法

ソフトウェア配信の実力を示すDORAの4指標(デプロイ頻度、変更のリードタイム、MTTR、変更失敗率)は、ビジネス成果と相関することが複数年の研究で示されてきました¹。DORA指標(DORA metrics)は、DevOpsにおける継続的デリバリやCI/CDの成熟度を測る代表的なベンチマークで、配信の速さと安定性の両面を定量化します。研究データでは、開発と運用のフローを定量化できた組織ほど意思決定が速く、顧客価値の提供も安定すると報告されています¹。一方で、現場のダッシュボードにはCPUやメモリといった低レイヤのメトリクスが溢れ、経営層が知りたい「カート投入率」や「リリースごとの解約率の変動」とは直結しないという断絶が残りがちです。だからこそ、アプリケーションから意味のあるCustom Metricsを発火し、プロダクトファネルやSLO(サービスレベル目標)とつなげる設計が鍵になります。私はCustom Metricsを「ドメインの語彙でシステムを語る装置」と捉えています。技術指標を土台にしつつ、業務そのものの状態を観測可能にすることが、DevとBizを同じ地図の上に乗せる最短距離です。なお、SLI(サービスレベル指標)はユーザ体験を代表する計測値、SLOはその目標値を指します。

Custom Metricsで何を測るか:技術指標と業務指標を同じ座標に

Custom Metricsの価値は、低レイヤのシグナルを置き換えるのではなく、ドメインの出来事と結び付けて階層化する点にあります。アプリケーションのエンドポイント単位ではRED(Rate、Errors、Duration:リクエストレート、エラー率、遅延)で安定性を可視化し²、インフラ資源ではUSE(Utilization、Saturation、Errors:使用率、飽和度、エラー)でボトルネックを察知します³。その上で「チェックアウト到達」「決済成功」「返金発生」といったビジネスイベントをCounterHistogramで記録し、期間集計やラベルの切り口でプロダクトKPIと接合します。SLI/SLOは接合面の関節として機能し、たとえば“checkout_success / checkout_attempt”をSLIに、エラーバジェット消費が閾値を超えたら新機能のリリースを抑制するような運用ポリシーを実装できます。コンテキストの運搬にはトレースIDやユーザセグメントのラベルが便利ですが、高カーディナリティと個人情報(PII)を避けるのが定石です⁴⁵。ユーザIDのような一意値ではなく、プラン種別やテナント区分のような有限集合に丸め、過度な分割を避けます。これにより、DORA指標の「変更失敗率」などの変化を、実際のユーザ影響(SLI)と一貫した座標で読み解けます。

ラベル設計とメトリクスタイプの選び分け

ラベルは意思決定で実際に切りたい観点に限定し、単位と命名は一貫させます。命名はプレフィックスで領域を表し、app_checkout_*のように用途が推測できる粒度にします。回数はCounter、現在値はGauge、遅延や金額など分布を見るものはHistogramが適します。集計の滑らかさが欲しければSummaryもありますが、分散収集ではヒストグラムの方が後段で合成しやすく、PrometheusやOpenTelemetry Metricsのエコシステムとも親和的です。ヒストグラムのバケットは、SLOの閾値と観測の事実に基づいて決めると良く、たとえばチェックアウトのp95を2秒にしたいなら、0.1, 0.3, 0.6, 1, 2, 3, 5のように密度を変えます。最初は粗く、実測に応じて改良するのが健全です。

ビジネスKPIとPromQLの距離を縮める

ダッシュボードにそのまま経営に効く「率」や「時間」を載せるために、集計式をPromQLやGrafanaのTransformで表現しておきます。チェックアウト成功率は、試行と成功の二つのカウンタから導出できます。以下は5分窓での成功率を出すクエリの例です。

sum(rate(app_checkout_success_total[5m]))
  /
sum(rate(app_checkout_attempt_total[5m]))

ヒストグラムからp95の遅延を出したいときは、histogram_quantileを使います。SLOの閾値そのものを注釈し、逸脱を即座に見つけられるようにしておくと、運用とプロダクトの会話が速くなります。こうしたCustom Metricsの式は、DORAの「変更のリードタイム」や「MTTR」の解釈にも接続でき、リリース管理の意思決定に直結します。

実装:PrometheusとOpenTelemetryでドメインを観測可能にする

実装はクライアントライブラリでメトリクスを埋め込み、エクスポートし、可視化の3層で考えます。以降は代表的な技術スタックでの実装例を示します。いずれもアプリのコードパスをブロックしないこと、エラー時は静かにフォールバックすることを徹底します。

Go + Prometheus:チェックアウト遅延のヒストグラムとトレース連携

package main

import (
  "log"
  "math/rand"
  "net/http"
  "time"

  "github.com/prometheus/client_golang/prometheus"
  "github.com/prometheus/client_golang/prometheus/promhttp"
)

var checkoutLatency = prometheus.NewHistogramVec(
  prometheus.HistogramOpts{
    Namespace: "app",
    Subsystem: "checkout",
    Name:      "latency_seconds",
    Help:      "Checkout latency",
    Buckets:   []float64{0.1, 0.3, 0.6, 1, 2, 3, 5},
  },
  []string{"tenant", "payment_provider"},
)

var checkoutAttempt = prometheus.NewCounterVec(
  prometheus.CounterOpts{
    Namespace: "app",
    Subsystem: "checkout",
    Name:      "attempt_total",
    Help:      "Checkout attempts",
  },
  []string{"tenant"},
)

var checkoutSuccess = prometheus.NewCounterVec(
  prometheus.CounterOpts{
    Namespace: "app",
    Subsystem: "checkout",
    Name:      "success_total",
    Help:      "Checkout successes",
  },
  []string{"tenant"},
)

func main() {
  reg := prometheus.NewRegistry()
  reg.MustRegister(checkoutLatency, checkoutAttempt, checkoutSuccess)

  http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
  http.HandleFunc("/checkout", func(w http.ResponseWriter, r *http.Request) {
    tenant := r.Header.Get("X-Tenant")
    if tenant == "" { tenant = "unknown" }
    start := time.Now()

    checkoutAttempt.WithLabelValues(tenant).Inc()

    // 擬似処理
    time.Sleep(time.Duration(100+rand.Intn(1200)) * time.Millisecond)

    // 成功/失敗の分岐(例)
    if rand.Float64() < 0.9 {
      checkoutSuccess.WithLabelValues(tenant).Inc()
      w.WriteHeader(http.StatusOK)
      _, _ = w.Write([]byte("ok"))
    } else {
      http.Error(w, "failed", http.StatusInternalServerError)
    }

    elapsed := time.Since(start).Seconds()
    checkoutLatency.WithLabelValues(tenant, "stripe").Observe(elapsed)
  })

  log.Fatal(http.ListenAndServe(":8080", nil))
}

ExemplarでトレースIDを添付すると遅延外れ値から該当トレースにジャンプできます。GoのOTel/Prom bridgeを使うか、PrometheusクライアントのObserveWithExemplarを利用します。

Node.js + prom-client:コンバージョンをラベルで切る

import express from 'express';
import client from 'prom-client';

const app = express();
const reg = new client.Registry();
client.collectDefaultMetrics({ register: reg });

const conversion = new client.Counter({
  name: 'app_checkout_success_total',
  help: 'Checkout successes',
  labelNames: ['tenant', 'campaign']
});

reg.registerMetric(conversion);

app.post('/checkout/success', (req, res) => {
  const tenant = req.header('X-Tenant') || 'unknown';
  const campaign = req.header('X-Campaign') || 'none';
  conversion.labels(tenant, campaign).inc();
  res.json({ ok: true });
});

app.get('/metrics', async (req, res) => {
  res.set('Content-Type', reg.contentType);
  res.end(await reg.metrics());
});

app.listen(3000);

キャンペーンは期間で有限集合に保つことでカーディナリティの暴発を避けます。終了後にメトリクスを新規に切り替えるより、ラベル値の寿命を短くする運用が安全です。

Python + OpenTelemetry Metrics:OTLPでCollectorへ

from fastapi import FastAPI
from time import perf_counter

from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry import metrics

app = FastAPI()

resource = Resource.create({"service.name": "checkout-api"})
exporter = OTLPMetricExporter(endpoint="http://otel-collector:4318/v1/metrics")
reader = PeriodicExportingMetricReader(exporter, export_interval_millis=10000)
provider = MeterProvider(resource=resource, metric_readers=[reader])
metrics.set_meter_provider(provider)
meter = metrics.get_meter("checkout")

hist = meter.create_histogram(
    name="app.checkout.latency", unit="s", description="Checkout latency"
)

@app.post("/checkout")
async def checkout():
    t0 = perf_counter()
    # ... business logic ...
    elapsed = perf_counter() - t0
    hist.record(elapsed, {"tenant": "pro"})
    return {"ok": True}

Collector側でPrometheusリモート書き込みやTempo/Jaeger連携に分配できます。ビューでヒストグラムのバケット再構成も可能です。

Java + Spring Boot + Micrometer:SLOを意識した計測

@RestController
@RequestMapping("/api")
public class CheckoutController {
  private final Counter attempt;
  private final Counter success;
  private final Timer latency;

  public CheckoutController(MeterRegistry registry) {
    this.attempt = Counter.builder("app.checkout.attempt")
      .tag("tenant", "basic").register(registry);
    this.success = Counter.builder("app.checkout.success")
      .tag("tenant", "basic").register(registry);
    this.latency = Timer.builder("app.checkout.latency")
      .publishPercentileHistogram()
      .sla(Duration.ofSeconds(1), Duration.ofSeconds(2))
      .register(registry);
  }

  @PostMapping("/checkout")
  public ResponseEntity<String> checkout() {
    attempt.increment();
    return latency.record(() -> {
      // ... business logic ...
      success.increment();
      return ResponseEntity.ok("ok");
    });
  }
}

MicrometerはPrometheus出力やOTLPなど複数エクスポータに対応しています。SLA/SLOをメトリクスに埋めると、あとからの可視化が容易になります。

バッチのカウントにはPushgatewayを最小限で

echo "app_batch_processed_total 42" | curl --data-binary @- \
  http://pushgateway:9091/metrics/job/daily_summary/tenant/pro

プッシュは格納地点がボトルネックになりやすいので、バッチ専用に限定し、主経路はPullを維持します。

運用設計:SLO、アラート、ガバナンス、パフォーマンス

Custom Metricsの運用では、閾値ベースではなくエラーバジェットに紐づくポリシーが有効です。たとえばチェックアウト成功率が一定期間で目標を下回った場合にのみページやオンコールを発火し、短時間の揺らぎは吸収します。アラートはSLO違反の「症状」を監視し、個々のインスタンスのCPUなど「原因」はダッシュボードで掘りに行くのが静穏運用の近道です。以下はSLOに基づいたPrometheusのアラートルールの例です。

groups:
- name: slo.alerts
  rules:
  - alert: CheckoutLowSuccessRate
    expr: sum(rate(app_checkout_success_total[5m]))
          / sum(rate(app_checkout_attempt_total[5m])) < 0.98
    for: 15m
    labels:
      severity: page
    annotations:
      summary: "Checkout success rate below SLO"
      runbook: "/runbooks/checkout-slo"

データガバナンスの観点では、PIIをラベルに載せないこと、保持期間に応じたロールアップやダウンサンプリングを行うこと、ドメイン定義の変更はスキーマバージョンで管理することが重要です。たとえばcampaignラベルを新体系に移行する際には、新旧を併存させた上で可視化の切り替えを行い、古い系列は保守的に廃止します。カーディナリティの監視も欠かせません。Prometheusではtopkcount_valuesで異常なラベル爆発を早期に察知できます⁴⁵。なお、こうしたSLO駆動の運用は、DORAの「変更失敗率」や「MTTR」の改善にも寄与しやすく、リリース判断を透明化します。

オーバーヘッドと簡易ベンチ:設計で抑える

メトリクスは軽量ですがゼロコストではありません。一般的には、カウンタのインクリメントはナノ秒〜数十ナノ秒、ヒストグラムの観測はバケット更新やロックでナノ秒〜マイクロ秒程度のオーバーヘッドが追加されるとされます。ホットパスではサンプリングやバッファリング、遅延エクスポートを組み合わせると良いでしょう。以下はGoのクライアントでカウンタ増分とヒストグラム観測を比較するためのマイクロベンチマークの例です(結果は環境依存)。

package metricsbench

import (
  "testing"
  "github.com/prometheus/client_golang/prometheus"
)

var c = prometheus.NewCounter(prometheus.CounterOpts{Name: "bench_counter"})
var h = prometheus.NewHistogram(prometheus.HistogramOpts{Name: "bench_hist", Buckets: []float64{0.1, 1, 5}})

func BenchmarkCounter(b *testing.B) {
  for i := 0; i < b.N; i++ { c.Inc() }
}

func BenchmarkHistogram(b *testing.B) {
  for i := 0; i < b.N; i++ { h.Observe(0.5) }
}

高トラフィック路線でヒストグラムが熱くなる場合は、バケット数を減らす、収集間隔を延ばす、事後集計を使うと安定します。エクスポータやCollectorがダウンしてもアプリは動き続けるべきなので、送信失敗は例外にせず非同期・ベストエフォートで処理します。

可視化と連携:ダッシュボードの設計原則

ダッシュボードは役割ごとに質問を明確にします。現場向けにはRED/USEで異常の検知を早くし²³、経営向けにはコンバージョン、ARPU、チャーンといった業務指標を日次で俯瞰します。両者の橋渡しとして、業務KPIに寄与する技術要因のトップ寄与を並べる「因果の回廊」を用意すると、リリース判断が速くなります。Grafanaでは変数にtenantplanを用意し、組織の視線に合わせて切り替えられるようにしておくのが実用的です。あわせて、DORA指標(デプロイ頻度、変更リードタイム、MTTR、変更失敗率)をCustom MetricsやPromQLで近似し、可観測性(Observability)ダッシュボードの中に同居させると、DevOpsの成熟度評価と日々の運用をワンストップにできます。

ケース:リリース判断に使える“業務×技術”ビュー

小売ECのようなトランザクション系では、チェックアウトのp95遅延が一定しきい値(例:2〜3秒)を超えると成功率が数ポイント落ちる、といった相関がCustom Metricsから見えてくることがあります。これを踏まえ、SLO違反時には新機能のフラグを自動的にオフにし、該当のトレースをExemplarから追ってデータベースのクエリプラン退化を特定するといった運用は有効です。修正リリース後、同じビューで成功率の回復やリードタイムの短縮を確認できれば、経営判断(キャンペーン再開など)をダッシュボード起点で素早く行える可能性があります。この一連の流れは、「状態の可視化」から「意思決定の自動化」までをCustom Metricsが貫く好例です。重要なのは、最初から完璧なモデルを目指すのではなく、まずは最大の問いに答える最小のメトリクスから始め、観測に基づいて進化させることです。

よくある落とし穴と回避策

ラベルにユーザIDを入れてしまいストレージが膨張する、複数チームが勝手に命名してダッシュボードが乱立する、エラーバジェットを無視した“ノイジー”なアラートで疲弊する、といった問題は珍しくありません。これらは設計原則とガバナンスで避けられます。命名規約はリポジトリにコード化し、Lintで強制します。ダッシュボードはレビューの対象とし、KPIとの対応をPRのテンプレートに明記します。最後に、メトリクスはプロダクトの一部でありテレメトリの変更も仕様変更だという認識を、チームに共有しておくと運用が安定します⁴⁵。

まとめ:Custom Metricsを“意思決定API”にする

Custom Metricsは、システムの安定性を測るためだけの道具ではありません。ドメインの言葉で出来事を記録し、技術指標と業務指標を同じ座標に置くための媒介です。チェックアウト成功率やp95遅延のような指標をSLIに据え、SLOに紐づくアラートで運用の静穏を保ち、Exemplarやトレース連携で原因追跡の距離を縮める。この循環が回り始めると、リリースの可否、キャンペーンの開始、機能フラグの切り替えといった意思決定がダッシュボード起点で高速化されます。まずは自社の最重要な問いを一つ選び、その問いに答える最小のCustom Metricsをコードに埋め込んでみてください。次に、PrometheusやOpenTelemetryで集め、Grafanaで“業務×技術”のビューを一枚作る。小さな一歩でも、チームの認知は確実に変わります。その変化が積み重なった先に、あなたの組織だけの強いオペレーティングモデルが形になります。

参考文献

  1. Google Cloud Blog. The 2019 Accelerate State of DevOps: Elite performance, productivity, and scaling. https://cloud.google.com/blog/products/devops-sre/the-2019-accelerate-state-of-devops-elite-performance-productivity-and-scaling
  2. InfoWorld. The RED method: A new strategy for monitoring microservices. https://www.infoworld.com/article/2270578/the-red-method-a-new-strategy-for-monitoring-microservices.html
  3. Brendan Gregg. The USE Method. https://brendangregg.com/usemethod.html
  4. Grafana Labs Blog. How to manage high-cardinality metrics in Prometheus and Kubernetes. https://grafana.com/blog/2022/10/20/how-to-manage-high-cardinality-metrics-in-prometheus-and-kubernetes/
  5. PromLabs Blog. Avoid these 6 mistakes when getting started with Prometheus. https://promlabs.com/blog/2022/12/11/avoid-these-6-mistakes-when-getting-started-with-prometheus/