🎄 科学と神々株式会社アドベントカレンダー 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")
🌟 まとめ
エラーハンドリングとロギングの要点:
-
エラー処理
- try-except パターン
- Result型パターン
- カスタム例外型
-
構造化ロギング
- JSON形式ログ
- メタデータ付与
- リクエストID追跡
-
ユーザー体験
- わかりやすいエラーメッセージ
- 具体的な解決策提示
- 内部情報の非表示
-
監視とアラート
- ログローテーション
- Slack統合
- 重大度別アラート
前回: Day 18: デバイスフィンガープリント
次回: Day 20: 実際に動かしてみよう - セットアップガイド
Happy Learning! 🎉