監査ログ
📍 このドキュメントの位置づけ
対象読者: 監査ログの実装詳細を理解したい開発者
このドキュメントで学べること:
- 監査ログ(Audit Log)の構造
- AuditLogWriter プラグインの実装方法
- 非同期ログ処理の仕組み(AuditLogPublisher)
- データベースへの永続化
- カスタムログ出力先の実装(CloudWatch Logs、Splunk等)
前提知識:
🏗️ 監査ログアーキテクチャ
idp-serverは、すべての重要な操作を**監査ログ(Audit Log)**として記録します。
監査ログフロー
┌─────────────────────────────────────────────────────────────┐
│ 1. API操作(Control Plane / Application Plane) │
│ - ユーザー作成、設定変更、認証、トークン発行等 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. AuditLogCreator │
│ - AuditableContext から AuditLog を生成 │
│ - UUID生成、タイ ムスタンプ追加 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. AuditLogPublisher │
│ - 非同期イベント発行(Spring Events等) │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. AuditLogWriters │
│ - 各 AuditLogWriter の shouldExecute() 判定 │
│ - 実行すべき Writer の write() 実行 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. AuditLogWriter 実装 │
│ - AuditLogDataBaseWriter: データベースに保存 │
│ - カスタムWriter: CloudWatch Logs、Splunk等に送信 │
└─────────────────────────────────────────────────────────────┘
📦 AuditLog モデル
監査ログは、以下の情報を含みます。
public class AuditLog {
String id; // 監査ログID(UUID)
String type; // ログタイプ(例: "user.created")
String description; // 説明
String tenantId; // テナントID
String clientId; // クライアントID(オプション)
String userId; // ユーザーID(オプション)
String externalUserId; // 外部ユーザーID(オプション)
JsonNodeWrapper userPayload; // ユーザー情報
String targetResource; // 対象リソース(例: "user")
String targetResourceAction; // 操作(例: "create")
JsonNodeWrapper request; // リクエスト内容
JsonNodeWrapper before; // 変更前の状態
JsonNodeWrapper after; // 変更後の状態
String outcomeResult; // 結果(例: "success", "failure")
String outcomeReason; // 理由
String targetTenantId; // 対象テナントID(マルチテナント操作時)
String ipAddress; // IPアドレス
String userAgent; // User-Agent
JsonNodeWrapper attributes; // 追加属性
boolean dryRun; // Dry Run モード
LocalDateTime createdAt; // 作成日時
}
参考実装: AuditLog.java:25
AuditLog生成例
public class AuditLogCreator {
public static AuditLog create(AuditableContext context) {
String id = UUID.randomUUID().toString();
LocalDateTime createdAt = SystemDateTime.now();
return new AuditLog(
id,
context.type(), // "user.created"
context.description(), // "User registration"
context.tenantId(),
context.clientId(),
context.userId(),
context.externalUserId(),
JsonNodeWrapper.fromMap(context.userPayload()),
context.targetResource(), // "user"
context.targetResourceAction(), // "create"
JsonNodeWrapper.fromMap(context.request()),
JsonNodeWrapper.fromMap(context.before()),
JsonNodeWrapper.fromMap(context.after()),
context.outcomeResult(), // "success"
context.outcomeReason(),
context.targetTenantId(),
context.ipAddress(),
context.userAgent(),
JsonNodeWrapper.fromMap(context.attributes()),
context.dryRun(),
createdAt);
}
}
参考実装: AuditLogCreator.java:25
🔌 AuditLogWriter プラグイン
インターフェース
public interface AuditLogWriter {
/**
* このWriterを実行すべきか判定
*
* @param tenant テナント情報
* @param auditLog 監 査ログ
* @return 実行する場合 true
*/
default boolean shouldExecute(Tenant tenant, AuditLog auditLog) {
return true; // デフォルトは常に実行
}
/**
* 監査ログを書き込む
*
* @param tenant テナント情報
* @param auditLog 監査ログ
*/
void write(Tenant tenant, AuditLog auditLog);
}
参考実装: AuditLogWriter.java:21
デフォルト実装: AuditLogDataBaseWriter
データベースに監査ログを保存します。
public class AuditLogDataBaseWriter implements AuditLogWriter {
AuditLogCommandRepository auditLogCommandRepository;
public AuditLogDataBaseWriter(AuditLogCommandRepository auditLogCommandRepository) {
this.auditLogCommandRepository = auditLogCommandRepository;
}
@Override
public void write(Tenant tenant, AuditLog auditLog) {
auditLogCommandRepository.register(tenant, auditLog);
}
}
参考実装: AuditLogDataBaseWriter.java:21
AuditLogWriters(複数Writer管理)
public class AuditLogWriters {
List<AuditLogWriter> writers;
public AuditLogWriters(List<AuditLogWriter> writers) {
this.writers = writers;
}
public void write(Tenant tenant, AuditLog auditLog) {
for (AuditLogWriter writer : writers) {
// shouldExecute() で判定
if (writer.shouldExecute(tenant, auditLog)) {
log.info(
"TenantId {} AuditLogWriter execute: {}",
tenant.identifierValue(),
writer.getClass().getSimpleName());
// 実行
writer.write(tenant, auditLog);
}
}
}
}
参考実装: AuditLogWriters.java:23
特徴:
- 複数のWriterを登録可能(データベース + CloudWatch Logs等)
shouldExecute()で条件判定(テナントごとに異なる出力先等)- すべてのWriterを順次実行
🔄 非同期処理: AuditLogPublisher
監査ログは、非同期で処理されます。
AuditLogPublisher インターフェース
public interface AuditLogPublisher {
/**
* 監査ログイベントを非同期処理のために発行
*
* @param auditLog 非同期処理される監査ログ
*/
void publish(AuditLog auditLog);
}
参考実装: AuditLogPublisher.java:28
実装パターン: Spring Events
// 1. Publisher実装(Spring Events使用)
@Component
public class SpringAuditLogPublisher implements AuditLogPublisher {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Override
public void publish(AuditLog auditLog) {
// Spring Eventとして発行
eventPublisher.publishEvent(new AuditLogEvent(auditLog));
}
}
// 2. Event定義
public class AuditLogEvent {
private final AuditLog auditLog;
public AuditLogEvent(AuditLog auditLog) {
this.auditLog = auditLog;
}
public AuditLog getAuditLog() {
return auditLog;
}
}
// 3. EventListener実装
@Component
public class AuditLogEventListener {
@Autowired
private AuditLogWriters auditLogWriters;
@Autowired
private TenantQueryRepository tenantRepository;
@EventListener
@Async // 非同期実行
public void handleAuditLogEvent(AuditLogEvent event) {
AuditLog auditLog = event.getAuditLog();
Tenant tenant = tenantRepository.get(auditLog.tenantIdentifier());
// すべてのWriterを実行
auditLogWriters.write(tenant, auditLog);
}
}
非同期処理のメリット:
- レスポンス速度向上: API応答を待たずにログ処理
- スケーラビリティ: ロ グ処理の負荷をバックグラウンド化
- 耐障害性: ログ出力エラーがAPIレスポンスに影響しない
🧩 カスタムAuditLogWriter実装例
例1: CloudWatch Logs Writer
public class CloudWatchLogsAuditLogWriter implements AuditLogWriter {
private final CloudWatchLogsClient cloudWatchClient;
private final String logGroupName;
private final String logStreamName;
public CloudWatchLogsAuditLogWriter(
CloudWatchLogsClient cloudWatchClient,
String logGroupName,
String logStreamName) {
this.cloudWatchClient = cloudWatchClient;
this.logGroupName = logGroupName;
this.logStreamName = logStreamName;
}
@Override
public boolean shouldExecute(Tenant tenant, AuditLog auditLog) {
// 本番環境のテナントのみCloudWatch Logsに出力
return tenant.type() == TenantType.PUBLIC
&& !auditLog.dryRun();
}
@Override
public void write(Tenant tenant, AuditLog auditLog) {
try {
// AuditLog を JSON 形式に変換
String logMessage = new ObjectMapper().writeValueAsString(auditLog.toMap());
// CloudWatch Logs に送信
InputLogEvent logEvent = InputLogEvent.builder()
.message(logMessage)
.timestamp(System.currentTimeMillis())
.build();
PutLogEventsRequest request = PutLogEventsRequest.builder()
.logGroupName(logGroupName)
.logStreamName(logStreamName)
.logEvents(logEvent)
.build();
cloudWatchClient.putLogEvents(request);
} catch (Exception e) {
// ログ出力エラーをログ(メタログ)
log.error("Failed to write audit log to CloudWatch Logs", e);
}
}
}
例2: Splunk Writer
public class SplunkAuditLogWriter implements AuditLogWriter {
private final HttpClient httpClient;
private final String splunkUrl;
private final String splunkToken;
@Override
public boolean shouldExecute(Tenant tenant, AuditLog auditLog) {
// 特定タイプのログのみSplunkに送信
return auditLog.type().startsWith("security.")
|| auditLog.outcomeResult().equals("failure");
}
@Override
public void write(Tenant tenant, AuditLog auditLog) {
try {
// Splunk HEC (HTTP Event Collector) 形式
Map<String, Object> event = Map.of(
"time", System.currentTimeMillis() / 1000,
"source", "idp-server",
"sourcetype", "audit_log",
"event", auditLog.toMap()
);
String json = new ObjectMapper().writeValueAsString(event);
// Splunk HEC に送信
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(splunkUrl + "/services/collector"))
.header("Authorization", "Splunk " + splunkToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
log.error("Failed to send audit log to Splunk: {}", response.body());
}
} catch (Exception e) {
log.error("Failed to write audit log to Splunk", e);
}
}
}
プラグイン登録
META-INF/services/org.idp.server.platform.audit.AuditLogWriterProvider
com.example.idp.audit.CloudWatchLogsAuditLogWriterProvider
com.example.idp.audit.SplunkAuditLogWriterProvider
AuditLogWriterProvider 実装:
public class CloudWatchLogsAuditLogWriterProvider implements AuditLogWriterProvider {
@Override
public AuditLogWriter provide(ApplicationComponentContainer container) {
CloudWatchLogsClient client = container.get(CloudWatchLogsClient.class);
String logGroupName = System.getenv("CLOUDWATCH_LOG_GROUP");
String logStreamName = System.getenv("CLOUDWATCH_LOG_STREAM");
return new CloudWatchLogsAuditLogWriter(client, logGroupName, logStreamName);
}
}
📋 監査ログのフィールド詳細
基本情報
| フィールド | 型 | 必須 | 説明 | 例 |
|---|---|---|---|---|
id | String | ✅ | 監査ログID(UUID) | "a1b2c3d4-..." |
type | String | ✅ | ログタイプ | "user.created" |
description | String | ✅ | 説明 | "User registration" |
tenantId | String | ✅ | テナントID | "tenant-123" |
createdAt | LocalDateTime | ✅ | 作成日時 | "2025-12-07T10:30:00" |
操作情報
| フィールド | 型 | 必須 | 説明 | 例 |
|---|---|---|---|---|
targetResource | String | ✅ | 対象リソース | "user" |
targetResourceAction | String | ✅ | 操作 | "create", "update", "delete" |
request | JsonNodeWrapper | リクエスト内容 | {"username": "test"} | |
before | JsonNodeWrapper | 変更前の状態 | {"status": "active"} | |
after | JsonNodeWrapper | 変更後の状態 | {"status": "inactive"} |
操作者情報
| フィールド | 型 | 必須 | 説明 | 例 |
|---|---|---|---|---|
clientId | String | クライアントID | "client-abc" | |
userId | String | ユーザーID | "user-xyz" | |
externalUserId | String | 外部ユーザーID | "external-123" | |
userPayload | JsonNodeWrapper | ユーザー情報 | {"email": "test@example.com"} | |
ipAddress | String | IPアドレス | "192.168.1.1" | |
userAgent | String | User-Agent | "Mozilla/5.0..." |
結果情報
| フィールド | 型 | 必須 | 説明 | 例 |
|---|---|---|---|---|
outcomeResult | String | ✅ | 結果 | "success", "failure" |
outcomeReason | String | 理由 | "Invalid credentials" | |
dryRun | boolean | ✅ | Dry Runモード | false |
追加情報
| フィールド | 型 | 必須 | 説明 | 例 |
|---|---|---|---|---|
targetTenantId | String | 対象テナントID | "tenant-456" | |
attributes | JsonNodeWrapper | 追加属性 | {"custom": "value"} |