0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

License System Day 19: エラーハンドリングとロギング

Last updated at Posted at 2025-12-18

🎄 科学と神々株式会社アドベントカレンダー 2025

License System Day 19: エラーハンドリングとロギング


📖 今日のテーマ

今日は、エラーハンドリングと構造化ロギングを学びます。

Nimの例外処理、Result型パターン、そして本番環境で必須の構造化ロギング実装を解説します。


🎯 エラーハンドリングの重要性

なぜ適切なエラー処理が必要か?

目的:
✅ システムの安定性向上
   → 予期しないクラッシュを防ぐ

✅ ユーザー体験の改善
   → わかりやすいエラーメッセージ

✅ デバッグの効率化
   → 詳細なログで問題を特定

✅ セキュリティ強化
   → 内部情報の漏洩を防ぐ

💻 Nimのエラーハンドリング

1. 例外処理(try-except)

# server/src/error_handling.nim
import std/[json, strutils]

type
  LicenseError* = object of CatchableError
    ## ライセンス関連エラー

  InvalidCredentialsError* = object of LicenseError
    ## 認証エラー

  RateLimitExceededError* = object of LicenseError
    ## レート制限超過エラー

  SubscriptionExpiredError* = object of LicenseError
    ## サブスクリプション期限切れエラー

proc activateLicense*(email, password: string): JsonNode =
  ## ライセンス認証(例外処理あり)

  try:
    # ユーザー検証
    let user = db.getUserByEmail(email)
    if user.isNone:
      raise newException(InvalidCredentialsError, "Invalid email or password")

    # パスワード検証
    if not verifyPassword(password, user.get.passwordHash):
      raise newException(InvalidCredentialsError, "Invalid email or password")

    # サブスクリプション確認
    let subscription = db.getActiveSubscription(user.get.userId)
    if subscription.isNone:
      raise newException(SubscriptionExpiredError, "No active subscription")

    # ライセンス生成
    result = %*{
      "status": "activated",
      "activation_key": generateJWT(user.get.userId),
      "plan_type": subscription.get.planType
    }

  except InvalidCredentialsError as e:
    # 認証エラー: ユーザーに表示
    raise

  except SubscriptionExpiredError as e:
    # サブスクリプション期限切れ: ユーザーに表示
    raise

  except Exception as e:
    # その他のエラー: 内部エラーとして処理
    logger.log(lvlError, "Unexpected error in activateLicense: " & e.msg)
    raise newException(LicenseError, "Internal server error")

2. Result型パターン

# より関数型プログラミング的なアプローチ
import std/options

type
  Result*[T, E] = object
    case ok: bool
    of true:
      value: T
    of false:
      error: E

proc ok*[T, E](value: T): Result[T, E] =
  Result[T, E](ok: true, value: value)

proc err*[T, E](error: E): Result[T, E] =
  Result[T, E](ok: false, error: error)

proc isOk*[T, E](self: Result[T, E]): bool = self.ok
proc isErr*[T, E](self: Result[T, E]): bool = not self.ok

proc get*[T, E](self: Result[T, E]): T =
  if not self.ok:
    raise newException(ValueError, "Result is not Ok")
  self.value

proc getError*[T, E](self: Result[T, E]): E =
  if self.ok:
    raise newException(ValueError, "Result is not Error")
  self.error

# 使用例
proc validateLicense*(token: string): Result[JsonNode, string] =
  ## Result型を使ったライセンス検証

  # JWT検証
  let claims = cryptoService.verifyJWT(token)
  if claims.isNone:
    return err[JsonNode, string]("Invalid or expired token")

  # ライセンス確認
  let license = db.getLicenseByKey(token)
  if license.isNone:
    return err[JsonNode, string]("License not found")

  # 成功
  let response = %*{
    "status": "valid",
    "premium": true,
    "plan_type": claims.get["plan_type"]
  }

  return ok[JsonNode, string](response)

# 使用側
let result = validateLicense(token)
if result.isOk:
  resp Http200, result.get()
else:
  resp Http401, %*{"error": result.getError()}

📝 構造化ロギング

Nimロギングシステム

# server/src/logger.nim
import std/[logging, times, json, os]

type
  StructuredLogger* = ref object
    baseLogger: FileLogger
    level: Level

  LogEntry* = object
    timestamp*: DateTime
    level*: string
    message*: string
    module*: string
    function*: string
    userId*: string
    requestId*: string
    metadata*: JsonNode

proc newStructuredLogger*(logPath: string, level: Level = lvlInfo): StructuredLogger =
  ## 構造化ロガーの作成

  let logger = newFileLogger(logPath, fmtStr = "", levelThreshold = level)

  result = StructuredLogger(
    baseLogger: logger,
    level: level
  )

proc log*(self: StructuredLogger, entry: LogEntry) =
  ## 構造化ログエントリの出力

  let jsonEntry = %*{
    "timestamp": entry.timestamp.format("yyyy-MM-dd'T'HH:mm:ss'Z'"),
    "level": entry.level,
    "message": entry.message,
    "module": entry.module,
    "function": entry.function,
    "user_id": entry.userId,
    "request_id": entry.requestId,
    "metadata": entry.metadata
  }

  self.baseLogger.log(self.level, $jsonEntry)

proc logInfo*(self: StructuredLogger, message: string,
             module, function: string = "",
             userId, requestId: string = "",
             metadata: JsonNode = newJNull()) =
  ## INFO レベルログ

  let entry = LogEntry(
    timestamp: now(),
    level: "INFO",
    message: message,
    module: module,
    function: function,
    userId: userId,
    requestId: requestId,
    metadata: metadata
  )

  self.log(entry)

proc logError*(self: StructuredLogger, message: string,
              module, function: string = "",
              userId, requestId: string = "",
              metadata: JsonNode = newJNull()) =
  ## ERROR レベルログ

  let entry = LogEntry(
    timestamp: now(),
    level: "ERROR",
    message: message,
    module: module,
    function: function,
    userId: userId,
    requestId: requestId,
    metadata: metadata
  )

  self.log(entry)

proc logWarning*(self: StructuredLogger, message: string,
                module, function: string = "",
                userId, requestId: string = "",
                metadata: JsonNode = newJNull()) =
  ## WARNING レベルログ

  let entry = LogEntry(
    timestamp: now(),
    level: "WARNING",
    message: message,
    module: module,
    function: function,
    userId: userId,
    requestId: requestId,
    metadata: metadata
  )

  self.log(entry)

# グローバルロガー
var appLogger* = newStructuredLogger("logs/application.log", lvlInfo)

ロギング統合

# server/src/main.nim
import jester

# リクエストIDミドルウェア
proc generateRequestId(): string =
  result = generateUUID()

routes:
  post "/api/v1/license/activate":
    let requestId = generateRequestId()

    try:
      # リクエストログ
      appLogger.logInfo(
        "License activation request",
        module = "main",
        function = "activate",
        requestId = requestId,
        metadata = %*{
          "email": body["email"].getStr(),
          "client_id": body["client_id"].getStr()
        }
      )

      # 処理実行
      let result = activateLicense(email, password)

      # 成功ログ
      appLogger.logInfo(
        "License activated successfully",
        module = "main",
        function = "activate",
        userId = userId,
        requestId = requestId,
        metadata = %*{
          "plan_type": result["plan_type"]
        }
      )

      resp Http200, result

    except InvalidCredentialsError as e:
      # 認証エラーログ
      appLogger.logWarning(
        "Invalid credentials",
        module = "main",
        function = "activate",
        requestId = requestId,
        metadata = %*{
          "error": e.msg
        }
      )

      resp Http401, %*{"error": "Invalid email or password"}

    except Exception as e:
      # システムエラーログ
      appLogger.logError(
        "Unexpected error in license activation",
        module = "main",
        function = "activate",
        requestId = requestId,
        metadata = %*{
          "error": e.msg,
          "stack_trace": getStackTrace(e)
        }
      )

      resp Http500, %*{"error": "Internal server error"}

🎨 ユーザーフレンドリーなエラーメッセージ

エラーの分類

# server/src/error_messages.nim
type
  ErrorCategory* = enum
    ecAuthentication    # 認証エラー
    ecAuthorization     # 認可エラー
    ecValidation        # バリデーションエラー
    ecRateLimit         # レート制限
    ecSubscription      # サブスクリプション
    ecSystem            # システムエラー

  UserFriendlyError* = object
    category*: ErrorCategory
    code*: string
    message*: string
    userMessage*: string
    httpStatus*: int
    suggestion*: string

const ErrorMessages* = {
  "INVALID_CREDENTIALS": UserFriendlyError(
    category: ecAuthentication,
    code: "INVALID_CREDENTIALS",
    message: "Invalid email or password",
    userMessage: "メールアドレスまたはパスワードが正しくありません",
    httpStatus: 401,
    suggestion: "入力内容をご確認の上、再度お試しください"
  ),

  "TOKEN_EXPIRED": UserFriendlyError(
    category: ecAuthentication,
    code: "TOKEN_EXPIRED",
    message: "Authentication token has expired",
    userMessage: "認証トークンの有効期限が切れました",
    httpStatus: 401,
    suggestion: "再度ログインしてください"
  ),

  "RATE_LIMIT_EXCEEDED": UserFriendlyError(
    category: ecRateLimit,
    code: "RATE_LIMIT_EXCEEDED",
    message: "Too many requests",
    userMessage: "リクエスト数が制限を超えました",
    httpStatus: 429,
    suggestion: "しばらく時間をおいてから再度お試しいただくか、プランのアップグレードをご検討ください"
  ),

  "SUBSCRIPTION_EXPIRED": UserFriendlyError(
    category: ecSubscription,
    code: "SUBSCRIPTION_EXPIRED",
    message: "Subscription has expired",
    userMessage: "サブスクリプションの有効期限が切れています",
    httpStatus: 402,
    suggestion: "サブスクリプションを更新してください"
  ),

  "DEVICE_LIMIT_EXCEEDED": UserFriendlyError(
    category: ecSubscription,
    code: "DEVICE_LIMIT_EXCEEDED",
    message: "Device limit exceeded for current plan",
    userMessage: "デバイス数が上限に達しました",
    httpStatus: 403,
    suggestion: "不要なデバイスを削除するか、プランをアップグレードしてください"
  ),

  "INTERNAL_ERROR": UserFriendlyError(
    category: ecSystem,
    code: "INTERNAL_ERROR",
    message: "An unexpected error occurred",
    userMessage: "予期しないエラーが発生しました",
    httpStatus: 500,
    suggestion: "しばらく時間をおいてから再度お試しください。問題が解決しない場合はサポートにお問い合わせください"
  )
}.toTable

proc getErrorResponse*(errorCode: string, requestId: string = ""): JsonNode =
  ## ユーザーフレンドリーなエラーレスポンス生成

  let error = ErrorMessages.getOrDefault(errorCode, ErrorMessages["INTERNAL_ERROR"])

  result = %*{
    "error": {
      "code": error.code,
      "message": error.userMessage,
      "suggestion": error.suggestion,
      "request_id": requestId
    }
  }

📊 ログ分析と監視

ログローテーション

# server/src/log_rotation.nim
import std/[os, times, strutils]

proc rotateLogsDaily*(logDir: string) =
  ## 日次ログローテーション

  let today = now().format("yyyy-MM-dd")
  let logFile = logDir / "application.log"

  if fileExists(logFile):
    # ファイルサイズチェック
    let fileInfo = getFileInfo(logFile)
    if fileInfo.size > 100_000_000:  # 100MB以上
      # アーカイブ
      let archiveName = "application_" & today & ".log"
      moveFile(logFile, logDir / "archive" / archiveName)

      # gzip圧縮
      execShellCmd("gzip " & logDir / "archive" / archiveName)

監視とアラート

# server/src/monitoring.nim
import std/[json, httpclient, asyncdispatch]

proc sendSlackAlert*(message: string, level: string = "error") {.async.} =
  ## Slackへアラート送信

  let webhookUrl = getEnv("SLACK_WEBHOOK_URL")
  if webhookUrl == "":
    return

  let payload = %*{
    "text": message,
    "attachments": [{
      "color": if level == "error": "danger" else: "warning",
      "fields": [
        {
          "title": "Timestamp",
          "value": now().format("yyyy-MM-dd HH:mm:ss"),
          "short": true
        },
        {
          "title": "Level",
          "value": level,
          "short": true
        }
      ]
    }]
  }

  let client = newAsyncHttpClient()
  client.headers = newHttpHeaders({"Content-Type": "application/json"})

  try:
    discard await client.postContent(webhookUrl, $payload)
  except:
    echo "Failed to send Slack alert"

# エラー発生時のアラート
proc logErrorWithAlert*(message: string, metadata: JsonNode = newJNull()) =
  appLogger.logError(message, metadata = metadata)

  # 重大なエラーの場合はSlackアラート
  if metadata.hasKey("severity") and metadata["severity"].getStr() == "critical":
    asyncCheck sendSlackAlert("🚨 Critical Error: " & message, "error")

🌟 まとめ

エラーハンドリングとロギングの要点:

  1. エラー処理

    • try-except パターン
    • Result型パターン
    • カスタム例外型
  2. 構造化ロギング

    • JSON形式ログ
    • メタデータ付与
    • リクエストID追跡
  3. ユーザー体験

    • わかりやすいエラーメッセージ
    • 具体的な解決策提示
    • 内部情報の非表示
  4. 監視とアラート

    • ログローテーション
    • Slack統合
    • 重大度別アラート

前回: Day 18: デバイスフィンガープリント
次回: Day 20: 実際に動かしてみよう - セットアップガイド

Happy Learning! 🎉

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?