Skilore

ログとモニタリング

エラーは発生する。重要なのは「素早く検知し、原因を特定し、修正する」こと。構造化ログ、エラートラッキング(Sentry)、アラート設計のベストプラクティスを解説。

81 分で読めます40,315 文字

ログとモニタリング

エラーは発生する。重要なのは「素早く検知し、原因を特定し、修正する」こと。構造化ログ、エラートラッキング(Sentry)、アラート設計のベストプラクティスを解説。

この章で学ぶこと

  • 構造化ログの設計を理解する
  • エラートラッキングサービスの活用を把握する
  • 効果的なアラート設計を学ぶ
  • 分散トレーシングの基礎を理解する
  • メトリクス収集とダッシュボード設計を習得する
  • ログのセキュリティとコンプライアンスを把握する

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:

  • 基本的なプログラミングの知識
  • 関連する基礎概念の理解
  • APIエラー設計 の内容を理解していること

1. 構造化ログ

1.1 構造化ログ vs 非構造化ログ

非構造化ログ(従来):
  [2025-01-15 10:30:45] ERROR: Failed to process order 12345 for user abc
  → 人間には読みやすいが、機械処理が困難
  → grep で検索するしかない
  → 集計やダッシュボードに使いにくい

構造化ログ(推奨):
  {
    "timestamp": "2025-01-15T10:30:45.123Z",
    "level": "error",
    "message": "Failed to process order",
    "service": "order-service",
    "traceId": "abc-123",
    "orderId": "12345",
    "userId": "abc",
    "error": {
      "name": "PaymentError",
      "message": "Insufficient funds",
      "code": "PAYMENT_INSUFFICIENT_FUNDS"
    },
    "duration_ms": 1234
  }

利点:
  → JSON で検索・フィルタリング可能
  → ダッシュボードで集計可能
  → 自動アラートのトリガーに
  → ELK Stack, CloudWatch Logs Insights 等で分析可能
  → 構造化されているため型安全にできる

1.2 TypeScript: pino による構造化ロガー

// pino: 高性能構造化ロガー
import pino from 'pino';
 
// ロガーの設定
const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  formatters: {
    level(label) { return { level: label }; },
    bindings(bindings) {
      return {
        pid: bindings.pid,
        hostname: bindings.hostname,
        service: process.env.SERVICE_NAME ?? 'unknown',
        version: process.env.APP_VERSION ?? 'unknown',
        environment: process.env.NODE_ENV ?? 'development',
      };
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
  // 本番環境ではJSON、開発環境ではpretty
  transport: process.env.NODE_ENV === 'development'
    ? { target: 'pino-pretty', options: { colorize: true } }
    : undefined,
  // 機密情報の除去
  redact: {
    paths: [
      'req.headers.authorization',
      'req.headers.cookie',
      'req.body.password',
      'req.body.creditCard',
      'req.body.ssn',
      '*.password',
      '*.token',
      '*.secret',
    ],
    censor: '[REDACTED]',
  },
});
 
// 基本的な使い方
logger.info({ orderId: '12345', userId: 'abc' }, 'Order created');
 
logger.error(
  {
    orderId: '12345',
    error: { name: err.name, message: err.message, code: err.code },
    duration_ms: Date.now() - startTime,
  },
  'Order processing failed',
);
 
// 子ロガー: コンテキスト情報を自動付与
const orderLogger = logger.child({
  module: 'order-service',
  version: '2.1.0',
});
 
orderLogger.info({ orderId: '12345' }, 'Processing order');
// → { module: "order-service", version: "2.1.0", orderId: "12345", ... }

1.3 リクエストスコープのロガー

// Express ミドルウェア: リクエストごとにロガーを作成
import { randomUUID } from 'crypto';
import { AsyncLocalStorage } from 'async_hooks';
 
// AsyncLocalStorage でリクエストコンテキストを管理
const als = new AsyncLocalStorage<{
  traceId: string;
  logger: pino.Logger;
}>();
 
// ミドルウェア
function requestLoggerMiddleware(req: Request, res: Response, next: NextFunction) {
  const traceId = req.headers['x-trace-id'] as string
    ?? req.headers['x-request-id'] as string
    ?? randomUUID();
 
  const requestLogger = logger.child({
    traceId,
    method: req.method,
    path: req.path,
    ip: req.ip,
    userAgent: req.headers['user-agent'],
  });
 
  // レスポンスヘッダーにトレースIDを設定
  res.setHeader('X-Trace-Id', traceId);
 
  // リクエスト開始ログ
  const startTime = Date.now();
  requestLogger.info('Request started');
 
  // レスポンス完了時のログ
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    const logData = {
      statusCode: res.statusCode,
      duration_ms: duration,
      contentLength: res.getHeader('content-length'),
    };
 
    if (res.statusCode >= 500) {
      requestLogger.error(logData, 'Request completed with server error');
    } else if (res.statusCode >= 400) {
      requestLogger.warn(logData, 'Request completed with client error');
    } else {
      requestLogger.info(logData, 'Request completed');
    }
  });
 
  // AsyncLocalStorage にロガーを保存
  als.run({ traceId, logger: requestLogger }, () => {
    next();
  });
}
 
// どこからでもリクエストスコープのロガーを取得
function getLogger(): pino.Logger {
  const store = als.getStore();
  return store?.logger ?? logger;
}
 
function getTraceId(): string {
  const store = als.getStore();
  return store?.traceId ?? 'no-trace';
}
 
// サービス層での使用
class OrderService {
  async createOrder(data: CreateOrderInput): Promise<Order> {
    const log = getLogger();
 
    log.info({ data }, 'Creating order');
 
    try {
      const order = await this.repo.create(data);
      log.info({ orderId: order.id }, 'Order created successfully');
      return order;
    } catch (error) {
      log.error({ error, data }, 'Failed to create order');
      throw error;
    }
  }
}

1.4 Python: structlog による構造化ロガー

import structlog
import logging
import json
from datetime import datetime
 
 
# structlog の設定
def configure_logging(environment: str = "production"):
    """構造化ログの設定"""
 
    # 共通のプロセッサー
    shared_processors = [
        structlog.contextvars.merge_contextvars,
        structlog.stdlib.add_log_level,
        structlog.stdlib.add_logger_name,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.UnicodeDecoder(),
    ]
 
    if environment == "development":
        # 開発環境: 色付きの人間可読フォーマット
        structlog.configure(
            processors=[
                *shared_processors,
                structlog.dev.ConsoleRenderer(colors=True),
            ],
            wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG),
        )
    else:
        # 本番環境: JSON フォーマット
        structlog.configure(
            processors=[
                *shared_processors,
                structlog.processors.format_exc_info,
                structlog.processors.JSONRenderer(),
            ],
            wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
        )
 
 
# 使用例
logger = structlog.get_logger()
 
# 基本的なログ
logger.info("order_created", order_id="12345", user_id="abc", amount=1500)
 
# エラーログ
try:
    process_order(order)
except Exception as e:
    logger.error(
        "order_processing_failed",
        order_id=order.id,
        error=str(e),
        error_type=type(e).__name__,
        exc_info=True,
    )
 
# コンテキスト変数(リクエストスコープ)
import structlog.contextvars
 
# FastAPI ミドルウェア
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
 
app = FastAPI()
 
class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        trace_id = request.headers.get("x-trace-id", str(uuid.uuid4()))
 
        # コンテキスト変数にバインド
        structlog.contextvars.clear_contextvars()
        structlog.contextvars.bind_contextvars(
            trace_id=trace_id,
            method=request.method,
            path=str(request.url.path),
            client_ip=request.client.host if request.client else "unknown",
        )
 
        log = structlog.get_logger()
        log.info("request_started")
 
        start = time.monotonic()
        try:
            response = await call_next(request)
            duration = (time.monotonic() - start) * 1000
 
            log.info(
                "request_completed",
                status_code=response.status_code,
                duration_ms=round(duration, 2),
            )
            response.headers["X-Trace-Id"] = trace_id
            return response
        except Exception as e:
            duration = (time.monotonic() - start) * 1000
            log.error(
                "request_failed",
                error=str(e),
                duration_ms=round(duration, 2),
                exc_info=True,
            )
            raise

1.5 Go: slog による構造化ロガー

package main
 
import (
	"context"
	"log/slog"
	"os"
	"time"
)
 
// ロガーの設定
func setupLogger(env string) *slog.Logger {
	var handler slog.Handler
 
	if env == "production" {
		// 本番: JSON
		handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
			Level: slog.LevelInfo,
		})
	} else {
		// 開発: テキスト
		handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
			Level: slog.LevelDebug,
		})
	}
 
	return slog.New(handler)
}
 
// リクエストスコープのロガー
type contextKey string
 
const loggerKey contextKey = "logger"
 
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
	return context.WithValue(ctx, loggerKey, logger)
}
 
func LoggerFrom(ctx context.Context) *slog.Logger {
	if logger, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
		return logger
	}
	return slog.Default()
}
 
// HTTPミドルウェア
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		traceID := r.Header.Get("X-Trace-Id")
		if traceID == "" {
			traceID = uuid.New().String()
		}
 
		requestLogger := slog.Default().With(
			slog.String("trace_id", traceID),
			slog.String("method", r.Method),
			slog.String("path", r.URL.Path),
			slog.String("remote_addr", r.RemoteAddr),
		)
 
		ctx := WithLogger(r.Context(), requestLogger)
 
		start := time.Now()
		requestLogger.Info("request started")
 
		// レスポンスラッパー
		rw := &responseWriter{ResponseWriter: w, statusCode: 200}
		next.ServeHTTP(rw, r.WithContext(ctx))
 
		duration := time.Since(start)
		requestLogger.Info("request completed",
			slog.Int("status_code", rw.statusCode),
			slog.Duration("duration", duration),
		)
	})
}
 
// サービス層での使用
func (s *OrderService) CreateOrder(ctx context.Context, input CreateOrderInput) (*Order, error) {
	logger := LoggerFrom(ctx)
 
	logger.Info("creating order",
		slog.String("user_id", input.UserID),
		slog.Int("item_count", len(input.Items)),
	)
 
	order, err := s.repo.Create(ctx, input)
	if err != nil {
		logger.Error("failed to create order",
			slog.String("error", err.Error()),
			slog.String("user_id", input.UserID),
		)
		return nil, err
	}
 
	logger.Info("order created",
		slog.String("order_id", order.ID),
		slog.Float64("total", order.Total),
	)
	return order, nil
}

2. ログレベル

2.1 ログレベルの定義と使い分け

レベル用途
fatalアプリケーション停止を伴うエラー
例: DB接続不可、必須設定の欠如
→ 即座にアラート、オンコール対応
error操作の失敗。ユーザーに影響するエラー
例: API呼び出し失敗、データ保存失敗
→ エラートラッキング(Sentry)に送信
warn潜在的な問題。今は動くが注意が必要
例: 非推奨APIの使用、リトライ発生、閾値接近
→ 定期的にチェック
info重要なビジネスイベント
例: 注文完了、ユーザー登録、決済成功
→ ビジネスメトリクスの基盤
debug開発時のデバッグ情報
例: 変数の値、処理の分岐点
→ 本番では通常無効
trace詳細なトレース情報
例: 関数の入出力、SQLクエリ、HTTP通信の詳細
→ 問題調査時のみ一時的に有効化
環境ごとの推奨レベル:
  本番:       info 以上
  ステージング: debug 以上
  開発:       trace 以上

動的なログレベル変更:
  → 本番で問題調査時に一時的に debug に変更
  → 環境変数やAPI経由で変更可能にする
  → 一定時間後に自動で元に戻す

2.2 ログレベルの判断基準

// ログレベルの使い分けガイドライン
 
// FATAL: アプリケーションが起動・継続できない
logger.fatal({ port: 3000, error: err }, 'Failed to bind to port');
logger.fatal({ dsn: dbConfig.dsn }, 'Database connection failed on startup');
 
// ERROR: 操作が失敗した(ユーザーに影響がある)
logger.error({ orderId, error: err }, 'Failed to process payment');
logger.error({ userId, error: err }, 'Failed to send password reset email');
 
// WARN: 問題の予兆、ただし操作は完了した
logger.warn({ queueSize: 950, maxSize: 1000 }, 'Queue approaching capacity');
logger.warn({ attempt: 2, maxRetries: 3 }, 'Retry attempt for external API');
logger.warn({ deprecatedField: 'oldField' }, 'Deprecated field used in request');
 
// INFO: 重要なビジネスイベント
logger.info({ orderId, amount: 5000 }, 'Order completed');
logger.info({ userId, plan: 'premium' }, 'User upgraded subscription');
logger.info({ batch: 'daily-report', count: 1500 }, 'Batch processing completed');
 
// DEBUG: 開発・調査用の詳細情報
logger.debug({ userId, filters }, 'Searching users with filters');
logger.debug({ query, params, duration_ms: 45 }, 'SQL query executed');
 
// TRACE: 非常に詳細な情報
logger.trace({ headers, body }, 'Outgoing HTTP request');
logger.trace({ response, duration_ms: 123 }, 'Incoming HTTP response');

3. エラートラッキング

3.1 Sentry の設定と使用

// Sentry: エラートラッキングの設定
import * as Sentry from '@sentry/node';
 
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.APP_VERSION,
  serverName: process.env.HOSTNAME,
 
  // トレーシング設定
  tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1.0,
 
  // エラーフィルタリング
  beforeSend(event, hint) {
    const error = hint?.originalException;
 
    // 4xxエラーはSentryに送信しない
    if (error instanceof AppError && error.statusCode < 500) {
      return null;
    }
 
    // 特定のエラーを除外
    if (error instanceof AbortError) {
      return null;
    }
 
    // 機密情報の除去
    if (event.request?.headers) {
      delete event.request.headers['authorization'];
      delete event.request.headers['cookie'];
    }
 
    return event;
  },
 
  // パンくずリストのフィルタリング
  beforeBreadcrumb(breadcrumb) {
    // 機密URLをフィルタ
    if (breadcrumb.category === 'http' && breadcrumb.data?.url) {
      const url = new URL(breadcrumb.data.url);
      if (url.pathname.includes('/auth/')) {
        breadcrumb.data.url = url.origin + '/auth/[redacted]';
      }
    }
    return breadcrumb;
  },
 
  // 統合設定
  integrations: [
    new Sentry.Integrations.Http({ tracing: true }),
    new Sentry.Integrations.Express({ app }),
    new Sentry.Integrations.Postgres(),
  ],
});
 
// エラーのキャプチャ
async function processOrder(orderId: string): Promise<void> {
  try {
    await doProcessOrder(orderId);
  } catch (error) {
    Sentry.withScope(scope => {
      // タグ: フィルタリング用
      scope.setTag('feature', 'order-processing');
      scope.setTag('order_type', 'standard');
 
      // コンテキスト: 詳細情報
      scope.setContext('order', {
        orderId,
        userId: currentUser.id,
        amount: order.totalAmount,
        itemCount: order.items.length,
      });
 
      // ユーザー情報
      scope.setUser({
        id: currentUser.id,
        email: currentUser.email,
        subscription: currentUser.plan,
      });
 
      // フィンガープリント: エラーのグルーピング
      scope.setFingerprint([
        'order-processing',
        error instanceof HttpError ? String(error.statusCode) : 'unknown',
      ]);
 
      // エラーレベル
      scope.setLevel('error');
 
      Sentry.captureException(error);
    });
 
    throw error;
  }
}

3.2 パフォーマンスモニタリング

// Sentry: パフォーマンスモニタリング
import * as Sentry from '@sentry/node';
 
// カスタムトランザクション
async function processPayment(paymentData: PaymentData): Promise<PaymentResult> {
  return Sentry.startSpan(
    {
      name: 'processPayment',
      op: 'payment.process',
      attributes: {
        'payment.amount': paymentData.amount,
        'payment.currency': paymentData.currency,
      },
    },
    async (span) => {
      // 子スパン: バリデーション
      const validationResult = await Sentry.startSpan(
        { name: 'validatePayment', op: 'validation' },
        async () => validatePaymentData(paymentData),
      );
 
      // 子スパン: 外部API呼び出し
      const chargeResult = await Sentry.startSpan(
        {
          name: 'chargePaymentProvider',
          op: 'http.client',
          attributes: { 'http.url': 'https://api.stripe.com/v1/charges' },
        },
        async () => stripe.charges.create(paymentData),
      );
 
      // 子スパン: DB保存
      await Sentry.startSpan(
        { name: 'savePaymentRecord', op: 'db.query' },
        async () => db.payments.create({ data: chargeResult }),
      );
 
      return chargeResult;
    },
  );
}

3.3 Python: Sentry 統合

import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
from sentry_sdk.integrations.aiohttp import AioHttpIntegration
 
 
def init_sentry(dsn: str, environment: str, release: str):
    """Sentryの初期化"""
    sentry_sdk.init(
        dsn=dsn,
        environment=environment,
        release=release,
        traces_sample_rate=0.1 if environment == "production" else 1.0,
        profiles_sample_rate=0.1,  # プロファイリング
        integrations=[
            FastApiIntegration(),
            SqlalchemyIntegration(),
            AioHttpIntegration(),
        ],
        before_send=before_send_filter,
    )
 
 
def before_send_filter(event, hint):
    """送信前フィルタリング"""
    exception = hint.get("exc_info")
    if exception:
        exc_type, exc_value, _ = exception
        # 4xxエラーは送信しない
        if isinstance(exc_value, AppError) and exc_value.status_code < 500:
            return None
    return event
 
 
# エラーキャプチャ
async def process_order(order_id: str):
    try:
        await do_process_order(order_id)
    except Exception as e:
        with sentry_sdk.push_scope() as scope:
            scope.set_tag("feature", "order-processing")
            scope.set_context("order", {
                "order_id": order_id,
                "user_id": current_user.id,
            })
            scope.set_user({
                "id": current_user.id,
                "email": current_user.email,
            })
            sentry_sdk.capture_exception(e)
        raise
 
 
# カスタムスパン
async def fetch_user_data(user_id: str) -> dict:
    with sentry_sdk.start_span(op="http.client", description="fetch user data"):
        async with aiohttp.ClientSession() as session:
            async with session.get(f"{USER_SERVICE_URL}/users/{user_id}") as resp:
                return await resp.json()

4. 分散トレーシング

4.1 OpenTelemetry の基礎

分散トレーシングの概念:
  → マイクロサービス間のリクエストを追跡
  → 1つのユーザーリクエストが複数のサービスを経由する場合に有用
  → ボトルネックの特定、エラーの発生箇所の特定

用語:
  Trace: 1つのリクエストの全体的な流れ
  Span: Trace 内の個々の処理単位
  Context: Span 間で伝播するメタデータ
  Baggage: サービス間で伝播するカスタムデータ

  例:
  Trace: ユーザーの注文リクエスト
    ├─ Span: API Gateway (10ms)
    ├─ Span: Order Service (200ms)
    │   ├─ Span: DB Query - Create Order (50ms)
    │   └─ Span: Payment Service Call (120ms)
    │       ├─ Span: Stripe API Call (80ms)
    │       └─ Span: DB Query - Save Payment (20ms)
    └─ Span: Notification Service (30ms)
        └─ Span: SendGrid API Call (25ms)
// OpenTelemetry の設定
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
 
const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'order-service',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
    [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
  }),
  traceExporter: new OTLPTraceExporter({
    url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/traces',
  }),
  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter({
      url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT + '/v1/metrics',
    }),
    exportIntervalMillis: 60000,
  }),
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-http': {
        ignoreIncomingPaths: ['/health', '/metrics'],
      },
      '@opentelemetry/instrumentation-express': {},
      '@opentelemetry/instrumentation-pg': {},
    }),
  ],
});
 
sdk.start();
 
// カスタムスパン
import { trace, SpanStatusCode } from '@opentelemetry/api';
 
const tracer = trace.getTracer('order-service');
 
async function processOrder(order: Order): Promise<ProcessResult> {
  return tracer.startActiveSpan('processOrder', async (span) => {
    span.setAttribute('order.id', order.id);
    span.setAttribute('order.amount', order.totalAmount);
    span.setAttribute('order.item_count', order.items.length);
 
    try {
      // バリデーション
      await tracer.startActiveSpan('validateOrder', async (validationSpan) => {
        await validateOrder(order);
        validationSpan.setStatus({ code: SpanStatusCode.OK });
        validationSpan.end();
      });
 
      // 決済処理
      const paymentResult = await tracer.startActiveSpan(
        'processPayment',
        async (paymentSpan) => {
          paymentSpan.setAttribute('payment.provider', 'stripe');
          const result = await paymentService.charge(order);
          paymentSpan.setAttribute('payment.id', result.paymentId);
          paymentSpan.setStatus({ code: SpanStatusCode.OK });
          paymentSpan.end();
          return result;
        },
      );
 
      span.setStatus({ code: SpanStatusCode.OK });
      return { orderId: order.id, paymentId: paymentResult.paymentId };
    } catch (error) {
      span.setStatus({
        code: SpanStatusCode.ERROR,
        message: (error as Error).message,
      });
      span.recordException(error as Error);
      throw error;
    } finally {
      span.end();
    }
  });
}

4.2 トレースコンテキストの伝播

// サービス間のトレースコンテキスト伝播
 
// HTTP ヘッダーで伝播(W3C Trace Context)
// traceparent: 00-trace_id-span_id-trace_flags
// tracestate: vendor-specific-data
 
// サービスAからサービスBを呼ぶ場合
import { context, propagation } from '@opentelemetry/api';
 
async function callOrderService(orderData: OrderInput): Promise<Order> {
  const headers: Record<string, string> = {};
 
  // 現在のコンテキストをHTTPヘッダーに注入
  propagation.inject(context.active(), headers);
 
  const response = await fetch('http://order-service/api/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...headers, // traceparent, tracestate が含まれる
    },
    body: JSON.stringify(orderData),
  });
 
  return response.json();
}
 
// サービスBでコンテキストを抽出
function extractContextMiddleware(req: Request, res: Response, next: NextFunction) {
  // HTTPヘッダーからコンテキストを抽出
  const extractedContext = propagation.extract(context.active(), req.headers);
 
  // 抽出したコンテキストでリクエストを処理
  context.with(extractedContext, () => {
    next();
  });
}

5. メトリクス

5.1 Prometheus メトリクス

// Prometheus メトリクスの収集
import { Counter, Histogram, Gauge, Summary, Registry } from 'prom-client';
 
const register = new Registry();
 
// デフォルトメトリクスの収集(CPU, メモリ等)
import { collectDefaultMetrics } from 'prom-client';
collectDefaultMetrics({ register });
 
// カスタムメトリクス
 
// カウンター: 単調増加する値
const httpRequestsTotal = new Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'path', 'status_code'],
  registers: [register],
});
 
// ヒストグラム: 値の分布
const httpRequestDuration = new Histogram({
  name: 'http_request_duration_seconds',
  help: 'HTTP request duration in seconds',
  labelNames: ['method', 'path', 'status_code'],
  buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
  registers: [register],
});
 
// ゲージ: 上下する値
const activeConnections = new Gauge({
  name: 'active_connections',
  help: 'Number of active connections',
  registers: [register],
});
 
const queueSize = new Gauge({
  name: 'job_queue_size',
  help: 'Number of jobs in the queue',
  labelNames: ['queue_name'],
  registers: [register],
});
 
// サマリー: パーセンタイル
const dbQueryDuration = new Summary({
  name: 'db_query_duration_seconds',
  help: 'Database query duration in seconds',
  labelNames: ['query_type', 'table'],
  percentiles: [0.5, 0.9, 0.95, 0.99],
  registers: [register],
});
 
// ミドルウェアでメトリクスを収集
function metricsMiddleware(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();
  activeConnections.inc();
 
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const labels = {
      method: req.method,
      path: req.route?.path ?? req.path,
      status_code: String(res.statusCode),
    };
 
    httpRequestsTotal.inc(labels);
    httpRequestDuration.observe(labels, duration);
    activeConnections.dec();
  });
 
  next();
}
 
// メトリクスエンドポイント
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});
 
// ビジネスメトリクス
const ordersCreated = new Counter({
  name: 'orders_created_total',
  help: 'Total number of orders created',
  labelNames: ['status', 'payment_method'],
  registers: [register],
});
 
const orderAmount = new Histogram({
  name: 'order_amount_jpy',
  help: 'Order amount in JPY',
  buckets: [100, 500, 1000, 5000, 10000, 50000, 100000],
  registers: [register],
});
 
// 使用例
async function createOrder(data: CreateOrderInput): Promise<Order> {
  const order = await orderRepo.create(data);
 
  ordersCreated.inc({
    status: 'success',
    payment_method: data.paymentMethod,
  });
  orderAmount.observe(order.totalAmount);
 
  return order;
}

5.2 RED メソッド

RED メソッド(サービスのモニタリング):
  R - Rate:     リクエスト数/秒
  E - Errors:   エラー数/秒(またはエラー率)
  D - Duration: レイテンシ(P50, P95, P99)

  → マイクロサービスの健全性を3つのメトリクスで把握

USE メソッド(リソースのモニタリング):
  U - Utilization: 使用率(CPU, メモリ, ディスク)
  S - Saturation:  飽和度(キュー長、待ちスレッド数)
  E - Errors:      エラー数

  → インフラリソースの健全性を把握

Four Golden Signals(Google SRE):
  1. Latency:    レスポンス時間
  2. Traffic:    リクエスト数
  3. Errors:     エラー率
  4. Saturation: リソース飽和度

6. アラート設計

6.1 アラートの原則

アラートの原則:
  1. アクション可能(受けたら何かできる)
     → 「エラーが発生しました」ではなく「決済処理のエラー率が5%を超えました」
     → アラートにランブック(対応手順書)へのリンクを含める

  2. 低ノイズ(誤報が少ない)
     → アラート疲れ(Alert Fatigue)を防ぐ
     → 閾値は十分に吟味する
     → 一時的なスパイクに反応しすぎない

  3. 適切な宛先(オンコール担当者)
     → 緊急度に応じたエスカレーション
     → Critical: PagerDuty → SMS/電話
     → Warning: Slack通知 → 翌営業日対応

  4. コンテキスト付き
     → ダッシュボードへのリンク
     → 関連ログへのリンク
     → ランブックへのリンク
     → 影響範囲の概要

6.2 アラートルールの設計

エラー率ベース:
  → 5xxエラー率 > 1%(5分間)→ Warning
  → 5xxエラー率 > 5%(5分間)→ Critical
  → 特定エンドポイントのエラー率 > 10% → Critical

レイテンシベース:
  → P95 > 2秒(5分間)→ Warning
  → P99 > 5秒(5分間)→ Critical
  → P50 > 1秒(持続的)→ Warning(性能劣化の兆候)

ビジネスメトリクス:
  → 決済成功率 < 95%(10分間)→ Critical
  → 注文数が前時間比 50% 減 → Warning
  → 新規登録数が前日比 70% 減 → Warning

インフラメトリクス:
  → CPU使用率 > 80%(15分間)→ Warning
  → CPU使用率 > 95%(5分間)→ Critical
  → メモリ使用率 > 85% → Warning
  → ディスク使用率 > 90% → Critical
  → DB接続プール使用率 > 80% → Warning

サーキットブレーカー:
  → 任意のサーキットブレーカーが Open → Warning
  → 主要サービスのサーキットブレーカーが Open → Critical

6.3 Prometheus Alertmanager の設定

# Prometheus アラートルール
groups:
  - name: http_alerts
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(http_requests_total{status_code=~"5.."}[5m]))
          / sum(rate(http_requests_total[5m]))
          > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High 5xx error rate ({{ $value | humanizePercentage }})"
          description: "5xxエラー率が5%を超えています"
          runbook_url: "https://wiki.example.com/runbooks/high-error-rate"
          dashboard: "https://grafana.example.com/d/http/overview"
 
      - alert: HighLatency
        expr: |
          histogram_quantile(0.95,
            sum(rate(http_request_duration_seconds_bucket[5m])) by (le)
          ) > 2
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High P95 latency ({{ $value | humanizeDuration }})"
          description: "P95レイテンシが2秒を超えています"
 
      - alert: HighQueueSize
        expr: job_queue_size > 1000
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "Job queue size is high ({{ $value }})"
          description: "ジョブキューのサイズが1000を超えています"
 
  - name: business_alerts
    rules:
      - alert: LowPaymentSuccessRate
        expr: |
          sum(rate(payments_total{status="success"}[10m]))
          / sum(rate(payments_total[10m]))
          < 0.95
        for: 10m
        labels:
          severity: critical
          team: payments
        annotations:
          summary: "Payment success rate below 95% ({{ $value | humanizePercentage }})"
          description: "決済成功率が95%を下回っています"
 
# Alertmanager の設定
route:
  receiver: default
  group_by: [alertname, severity]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: critical
      receiver: pagerduty-critical
      continue: true
    - match:
        severity: warning
      receiver: slack-warnings
 
receivers:
  - name: default
    slack_configs:
      - channel: '#alerts'
        send_resolved: true
 
  - name: pagerduty-critical
    pagerduty_configs:
      - service_key: '<pagerduty-key>'
        severity: critical
 
  - name: slack-warnings
    slack_configs:
      - channel: '#alerts-warnings'
        send_resolved: true
        title: '{{ .CommonAnnotations.summary }}'
        text: '{{ .CommonAnnotations.description }}'

7. ログのセキュリティとコンプライアンス

7.1 機密情報の取り扱い

// 機密情報のマスキング
class LogSanitizer {
  private static readonly SENSITIVE_FIELDS = new Set([
    'password',
    'token',
    'secret',
    'authorization',
    'cookie',
    'creditCard',
    'ssn',
    'apiKey',
    'accessToken',
    'refreshToken',
  ]);
 
  private static readonly PII_PATTERNS = [
    // メールアドレス
    { pattern: /[\w.-]+@[\w.-]+\.\w+/g, replacement: '[email]' },
    // クレジットカード番号
    { pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, replacement: '[card]' },
    // 電話番号
    { pattern: /\b0\d{1,4}[-\s]?\d{1,4}[-\s]?\d{3,4}\b/g, replacement: '[phone]' },
  ];
 
  static sanitize(obj: Record<string, any>): Record<string, any> {
    const sanitized: Record<string, any> = {};
 
    for (const [key, value] of Object.entries(obj)) {
      if (this.SENSITIVE_FIELDS.has(key.toLowerCase())) {
        sanitized[key] = '[REDACTED]';
      } else if (typeof value === 'object' && value !== null) {
        sanitized[key] = this.sanitize(value);
      } else if (typeof value === 'string') {
        let sanitizedValue = value;
        for (const { pattern, replacement } of this.PII_PATTERNS) {
          sanitizedValue = sanitizedValue.replace(pattern, replacement);
        }
        sanitized[key] = sanitizedValue;
      } else {
        sanitized[key] = value;
      }
    }
 
    return sanitized;
  }
}
 
// pino のシリアライザーに統合
const logger = pino({
  serializers: {
    req(req) {
      return LogSanitizer.sanitize({
        method: req.method,
        url: req.url,
        headers: req.headers,
        body: req.body,
      });
    },
    err(err) {
      return {
        type: err.constructor.name,
        message: err.message,
        code: err.code,
        stack: process.env.NODE_ENV !== 'production' ? err.stack : undefined,
      };
    },
  },
});

7.2 ログの保持とローテーション

ログ保持ポリシー:

  ホットストレージ(高速検索、高コスト):
    → 直近 7-30 日
    → Elasticsearch, CloudWatch Logs
    → リアルタイム検索・分析

  ウォームストレージ(中速検索、中コスト):
    → 30-90 日
    → S3 Standard, GCS Standard
    → 必要時にクエリ

  コールドストレージ(低速、低コスト):
    → 90日 - 数年
    → S3 Glacier, GCS Coldline
    → コンプライアンス要件に基づく保持

  コンプライアンス要件:
    → GDPR: 個人データの保持期間制限
    → PCI DSS: 監査ログ1年以上保持
    → SOX: 財務関連ログ7年保持
    → HIPAA: 医療データ関連ログ6年保持

8. ダッシュボード設計

8.1 Grafana ダッシュボードの構成

推奨ダッシュボード構成:

  1. サービス概要ダッシュボード
     → リクエスト数/秒(Rate)
     → エラー率(Errors)
     → P50/P95/P99 レイテンシ(Duration)
     → アクティブ接続数
     → 直近のアラート一覧

  2. エンドポイント別ダッシュボード
     → エンドポイントごとのリクエスト数
     → エンドポイントごとのエラー率
     → エンドポイントごとのレイテンシ
     → トップ10 スロークエリ

  3. インフラダッシュボード
     → CPU使用率
     → メモリ使用率
     → ディスクI/O
     → ネットワークI/O
     → コンテナ数(Kubernetes)

  4. ビジネスダッシュボード
     → 注文数/時間
     → 決済成功率
     → 新規登録数
     → アクティブユーザー数

  5. 依存サービスダッシュボード
     → 各外部API のレスポンスタイム
     → サーキットブレーカーの状態
     → DB接続プールの使用率
     → キャッシュヒット率

8.2 SLI/SLO の設計

SLI(Service Level Indicator):
  → サービスの品質を測定する指標
  → 例: 可用性、レイテンシ、スループット

SLO(Service Level Objective):
  → SLI の目標値
  → 例: 可用性 99.9%、P95 レイテンシ < 200ms

エラーバジェット:
  → SLO を超えた分のエラーが許容される「予算」
  → 99.9% SLO = 月間約43分のダウンタイム予算
  → エラーバジェット消費時はリリースを抑制

実装例:
  SLI: 正常レスポンス率 = 200-499レスポンス / 全レスポンス
  SLO: 30日間で 99.9% 以上
  エラーバジェット: 0.1% = 約43分/月

  Prometheus クエリ:
    # 30日間のSLI
    sum(rate(http_requests_total{status_code!~"5.."}[30d]))
    / sum(rate(http_requests_total[30d]))

    # 残りエラーバジェット
    1 - (
      sum(increase(http_requests_total{status_code=~"5.."}[30d]))
      / (sum(increase(http_requests_total[30d])) * 0.001)
    )

実践演習

演習1: 基本的な実装

以下の要件を満たすコードを実装してください。

要件:

  • 入力データの検証を行うこと
  • エラーハンドリングを適切に実装すること
  • テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
    """基本的な実装パターンの演習"""
 
    def __init__(self):
        self.data = []
 
    def validate_input(self, value):
        """入力値の検証"""
        if value is None:
            raise ValueError("入力値がNoneです")
        return True
 
    def process(self, value):
        """データ処理のメインロジック"""
        self.validate_input(value)
        self.data.append(value)
        return self.data
 
    def get_results(self):
        """処理結果の取得"""
        return {
            'count': len(self.data),
            'data': self.data
        }
 
# テスト
def test_exercise1():
    ex = Exercise1()
    assert ex.process(1) == [1]
    assert ex.process(2) == [1, 2]
    assert ex.get_results()['count'] == 2
 
    try:
        ex.process(None)
        assert False, "例外が発生するべき"
    except ValueError:
        pass
 
    print("全テスト合格!")
 
test_exercise1()

演習2: 応用パターン

基本実装を拡張して、以下の機能を追加してください。

# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
 
class AdvancedExercise:
    """応用パターンの演習"""
 
    def __init__(self, max_size: int = 100):
        self._items: List[Dict] = []
        self._max_size = max_size
        self._created_at = datetime.now()
 
    def add(self, key: str, value: any) -> bool:
        """アイテムの追加(サイズ制限付き)"""
        if len(self._items) >= self._max_size:
            return False
        self._items.append({
            'key': key,
            'value': value,
            'timestamp': datetime.now().isoformat()
        })
        return True
 
    def find(self, key: str) -> Optional[Dict]:
        """キーによる検索"""
        for item in reversed(self._items):
            if item['key'] == key:
                return item
        return None
 
    def remove(self, key: str) -> bool:
        """キーによる削除"""
        for i, item in enumerate(self._items):
            if item['key'] == key:
                self._items.pop(i)
                return True
        return False
 
    def stats(self) -> Dict:
        """統計情報"""
        return {
            'total_items': len(self._items),
            'max_size': self._max_size,
            'usage_percent': len(self._items) / self._max_size * 100,
            'uptime': str(datetime.now() - self._created_at)
        }
 
# テスト
def test_advanced():
    ex = AdvancedExercise(max_size=3)
    assert ex.add("a", 1) == True
    assert ex.add("b", 2) == True
    assert ex.add("c", 3) == True
    assert ex.add("d", 4) == False  # サイズ制限
    assert ex.find("b")['value'] == 2
    assert ex.remove("b") == True
    assert ex.find("b") is None
    stats = ex.stats()
    assert stats['total_items'] == 2
    print("応用テスト全合格!")
 
test_advanced()

演習3: パフォーマンス最適化

以下のコードのパフォーマンスを改善してください。

# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
 
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
    """非効率な検索"""
    for i in range(len(data)):
        for j in range(i + 1, len(data)):
            if data[i] + data[j] == target:
                return (i, j)
    return (-1, -1)
 
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
    """ハッシュマップを使った効率的な検索"""
    seen = {}
    for i, num in enumerate(data):
        complement = target - num
        if complement in seen:
            return (seen[complement], i)
        seen[num] = i
    return (-1, -1)
 
# ベンチマーク
def benchmark():
    import random
    data = list(range(5000))
    random.shuffle(data)
    target = data[100] + data[4000]
 
    start = time.time()
    result1 = slow_search(data, target)
    slow_time = time.time() - start
 
    start = time.time()
    result2 = fast_search(data, target)
    fast_time = time.time() - start
 
    print(f"非効率版: {slow_time:.4f}秒")
    print(f"効率版:   {fast_time:.6f}秒")
    print(f"高速化率: {slow_time/fast_time:.0f}倍")
 
benchmark()

ポイント:

  • アルゴリズムの計算量を意識する
  • 適切なデータ構造を選択する
  • ベンチマークで効果を測定する

トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
 
# ロガーの設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
 
def debug_decorator(func):
    """関数の入出力をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"戻り値: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logger.error(f"例外発生: {func.__name__}: {e}")
            logger.error(traceback.format_exc())
            raise
    return wrapper
 
@debug_decorator
def process_data(items):
    """データ処理(デバッグ対象)"""
    if not items:
        raise ValueError("空のデータ")
    return [item * 2 for item in items]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
CPU負荷 cProfile, py-spy アルゴリズム改善、並列化
メモリリーク tracemalloc, objgraph 参照の適切な解放
I/Oボトルネック strace, iostat 非同期I/O、キャッシュ
DB遅延 EXPLAIN, slow query log インデックス、クエリ最適化

FAQ

Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?

実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。

Q2: 初心者がよく陥る間違いは何ですか?

基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。

Q3: 実務ではどのように活用されていますか?

このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。


まとめ

手法 目的 ツール例
構造化ログ 検索・分析可能なログ pino, structlog, slog
エラートラッキング エラーの集約・通知 Sentry, Datadog, Bugsnag
分散トレーシング サービス間の追跡 OpenTelemetry, Jaeger, Zipkin
メトリクス 数値データの監視 Prometheus, Grafana, Datadog
アラート 異常の即時通知 PagerDuty, Alertmanager, Opsgenie
ダッシュボード 可視化 Grafana, Kibana, Datadog
ログ管理 ログ収集・検索 ELK Stack, Loki, CloudWatch

次に読むべきガイド


参考文献

  1. Sentry Documentation. docs.sentry.io.
  2. Google SRE Book. "Monitoring Distributed Systems." O'Reilly, 2016.
  3. OpenTelemetry Documentation. opentelemetry.io.
  4. Prometheus Documentation. prometheus.io.
  5. Grafana Documentation. grafana.com/docs.
  6. Beyer, B. et al. "Site Reliability Engineering." O'Reilly, 2016.
  7. pino Documentation. github.com/pinojs/pino.
  8. structlog Documentation. structlog.org.
  9. Go slog Documentation. pkg.go.dev/log/slog.
  10. W3C Trace Context. w3.org/TR/trace-context.