【SIerが知るべきログ設計のベストプラクティス】

SIer Tech Blog
2025年3月24日
【SIerが知るべきログ設計のベストプラクティス】
業務システムの運用において、適切なログ設計は非常に重要な要素です。本記事では、SIerエンジニアが知っておくべきログ設計のベストプラクティスについて、実践的な例を交えながら解説します。
1. ログ設計の基本原則
1.1 ログの目的と種類
ログは以下の目的で使用されます:
- 障害対応:問題発生時の原因特定
- 監査:システムの利用状況の追跡
- パフォーマンス分析:システムのボトルネック特定
- セキュリティ監視:不正アクセスの検知
- 業務分析:ビジネス指標の収集
1.2 ログレベルの定義
// ログレベルの定義例
public enum LogLevel {
ERROR, // システムの動作に重大な影響がある問題
WARN, // 警告。対応が必要な潜在的な問題
INFO, // 通常の操作の記録
DEBUG, // デバッグ用の詳細情報
TRACE // 最も詳細なデバッグ情報
}
// ログレベル判定ロジックの例
@Service
public class LogLevelDeterminer {
public LogLevel determineLevel(Throwable error) {
if (error instanceof BusinessException) {
return LogLevel.WARN;
} else if (error instanceof SystemException) {
return LogLevel.ERROR;
} else if (error instanceof ValidationException) {
return LogLevel.INFO;
}
return LogLevel.ERROR;
}
}
1.3 ログフォーマット
// 構造化ログの実装例
@Slf4j
public class StructuredLogger {
private final ObjectMapper objectMapper;
public void logBusinessEvent(String eventType, Map<String, Object> data) {
LogEntry entry = LogEntry.builder()
.timestamp(Instant.now())
.eventType(eventType)
.data(data)
.build();
try {
String json = objectMapper.writeValueAsString(entry);
log.info(json);
} catch (JsonProcessingException e) {
log.error("Failed to serialize log entry", e);
}
}
}
// ログエントリの定義
@Data
@Builder
public class LogEntry {
private Instant timestamp;
private String eventType;
private String correlationId;
private String userId;
private Map<String, Object> data;
private LogLevel level;
private String source;
private Map<String, String> tags;
}
2. 実践的なログ実装パターン
2.1 トランザクションログ
// トランザクションログの実装例
@Aspect
@Component
public class TransactionLogger {
private final Logger logger = LoggerFactory.getLogger(TransactionLogger.class);
@Around("@annotation(Transactional)")
public Object logTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
// トランザクション開始ログ
logger.info("Transaction started: {}.{}", className, methodName);
try {
// メソッド実行
Object result = joinPoint.proceed();
// トランザクション成功ログ
logger.info("Transaction completed: {}.{}", className, methodName);
return result;
} catch (Exception e) {
// トランザクション失敗ログ
logger.error("Transaction failed: {}.{}", className, methodName, e);
throw e;
}
}
}
// トランザクションログの使用例
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderRequest request) {
// 注文処理のロジック
return processOrder(request);
}
}
2.2 監査ログ
// 監査ログの実装例
@Aspect
@Component
public class AuditLogger {
private final Logger auditLogger = LoggerFactory.getLogger("AUDIT");
private final ObjectMapper objectMapper;
private final SecurityContext securityContext;
@Around("@annotation(Audited)")
public Object logAuditEvent(ProceedingJoinPoint joinPoint) throws Throwable {
AuditEvent.Builder auditBuilder = AuditEvent.builder()
.timestamp(Instant.now())
.user(securityContext.getCurrentUser())
.action(joinPoint.getSignature().getName())
.target(joinPoint.getTarget().getClass().getSimpleName());
try {
// メソッド実行前の状態を記録
recordPreExecutionState(auditBuilder, joinPoint);
// メソッド実行
Object result = joinPoint.proceed();
// 成功結果の記録
auditBuilder.status("SUCCESS")
.result(objectMapper.writeValueAsString(result));
return result;
} catch (Exception e) {
// 失敗結果の記録
auditBuilder.status("FAILURE")
.errorMessage(e.getMessage());
throw e;
} finally {
// 監査ログの出力
AuditEvent event = auditBuilder.build();
auditLogger.info(objectMapper.writeValueAsString(event));
}
}
private void recordPreExecutionState(AuditEvent.Builder builder, ProceedingJoinPoint joinPoint) {
try {
Object[] args = joinPoint.getArgs();
builder.parameters(objectMapper.writeValueAsString(args));
} catch (JsonProcessingException e) {
logger.warn("Failed to serialize method arguments", e);
}
}
}
// 監査対象メソッドの例
@Service
public class UserService {
@Audited
public User updateUserProfile(Long userId, UserProfile profile) {
// プロフィール更新のロジック
return updateProfile(userId, profile);
}
}
2.3 パフォーマンスログ
// パフォーマンスログの実装例
@Aspect
@Component
public class PerformanceLogger {
private final Logger perfLogger = LoggerFactory.getLogger("PERFORMANCE");
private final MeterRegistry meterRegistry;
@Around("@annotation(Measured)")
public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
String metricName = className + "." + methodName;
Timer.Sample sample = Timer.start(meterRegistry);
try {
return joinPoint.proceed();
} finally {
long durationMs = sample.stop(Timer.builder(metricName)
.tag("class", className)
.tag("method", methodName)
.register(meterRegistry));
perfLogger.info("Method execution time: {}.{} - {}ms",
className, methodName, durationMs);
}
}
}
// パフォーマンス計測の使用例
@Service
public class ReportService {
@Measured
public Report generateReport(ReportRequest request) {
// レポート生成のロジック
return generateReportData(request);
}
}
3. ログの集中管理と分析
3.1 ログ収集基盤
// ログ収集の設定例
@Configuration
public class LoggingConfig {
@Bean
public LogstashTcpSocketAppender logstashAppender() {
LogstashTcpSocketAppender appender = new LogstashTcpSocketAppender();
appender.addDestination("logstash:5000");
LogstashEncoder encoder = new LogstashEncoder();
encoder.setCustomFields("{\"application\":\"order-service\"}");
appender.setEncoder(encoder);
return appender;
}
@Bean
public FileAppender fileAppender() {
FileAppender appender = new FileAppender();
appender.setFile("/var/log/application.log");
appender.setAppend(true);
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
appender.setEncoder(encoder);
return appender;
}
}
// ログローテーションの設定
@Configuration
public class LogRotationConfig {
@Bean
public RollingFileAppender rollingFileAppender() {
RollingFileAppender appender = new RollingFileAppender();
appender.setFile("/var/log/application.log");
TimeBasedRollingPolicy policy = new TimeBasedRollingPolicy();
policy.setFileNamePattern("/var/log/application-%d{yyyy-MM-dd}.log");
policy.setMaxHistory(30);
appender.setRollingPolicy(policy);
return appender;
}
}
3.2 ログ分析
// ログ分析ユーティリティの実装例
@Service
public class LogAnalyzer {
private final ElasticsearchClient esClient;
private final AlertService alertService;
public List<ErrorPattern> analyzeErrorPatterns(
Instant startTime,
Instant endTime) {
// エラーログの検索
SearchResponse<LogEntry> response = esClient.search(s -> s
.index("logs-*")
.query(q -> q
.bool(b -> b
.must(m -> m
.range(r -> r
.field("@timestamp")
.from(startTime.toString())
.to(endTime.toString())
)
)
.must(m -> m
.term(t -> t
.field("level")
.value("ERROR")
)
)
)
)
.aggregations("error_patterns", a -> a
.terms(t -> t
.field("error.type")
.size(10)
)
),
LogEntry.class
);
return processErrorPatterns(response);
}
public void detectAnomalies() {
// エラー率の監視
double errorRate = calculateErrorRate(Duration.ofMinutes(5));
if (errorRate > 0.05) { // 5%以上
alertService.sendAlert(
AlertLevel.WARNING,
"High error rate detected",
Map.of("error_rate", errorRate)
);
}
// レスポンスタイムの監視
double avgResponseTime = calculateAverageResponseTime(Duration.ofMinutes(5));
if (avgResponseTime > 1000) { // 1秒以上
alertService.sendAlert(
AlertLevel.WARNING,
"High response time detected",
Map.of("avg_response_time", avgResponseTime)
);
}
}
}
// ログ検索サービス
@Service
public class LogSearchService {
private final ElasticsearchClient esClient;
public SearchResult searchLogs(LogSearchCriteria criteria) {
BoolQuery.Builder queryBuilder = new BoolQuery.Builder();
// 時間範囲の条件
if (criteria.getStartTime() != null && criteria.getEndTime() != null) {
queryBuilder.must(m -> m
.range(r -> r
.field("@timestamp")
.from(criteria.getStartTime().toString())
.to(criteria.getEndTime().toString())
)
);
}
// ログレベルの条件
if (criteria.getLevel() != null) {
queryBuilder.must(m -> m
.term(t -> t
.field("level")
.value(criteria.getLevel())
)
);
}
// キーワード検索
if (criteria.getKeyword() != null) {
queryBuilder.must(m -> m
.match(t -> t
.field("message")
.query(criteria.getKeyword())
)
);
}
// 検索の実行
SearchResponse<LogEntry> response = esClient.search(s -> s
.index("logs-*")
.query(queryBuilder.build())
.from(criteria.getOffset())
.size(criteria.getLimit())
.sort(sort -> sort
.field(f -> f
.field("@timestamp")
.order(SortOrder.Desc)
)
),
LogEntry.class
);
return processSearchResult(response);
}
}
4. セキュリティログ
4.1 セキュリティイベントのログ記録
// セキュリティログの実装例
@Aspect
@Component
public class SecurityLogger {
private final Logger securityLogger = LoggerFactory.getLogger("SECURITY");
private final SecurityEventRepository eventRepository;
@Around("@annotation(Secured)")
public Object logSecurityEvent(ProceedingJoinPoint joinPoint) throws Throwable {
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
SecurityEvent event = SecurityEvent.builder()
.timestamp(Instant.now())
.principal(auth.getName())
.action(joinPoint.getSignature().getName())
.resource(joinPoint.getTarget().getClass().getSimpleName())
.ipAddress(getCurrentIpAddress())
.build();
try {
Object result = joinPoint.proceed();
event.setStatus(SecurityEventStatus.SUCCESS);
return result;
} catch (AccessDeniedException e) {
event.setStatus(SecurityEventStatus.ACCESS_DENIED);
event.setErrorMessage(e.getMessage());
throw e;
} catch (Exception e) {
event.setStatus(SecurityEventStatus.ERROR);
event.setErrorMessage(e.getMessage());
throw e;
} finally {
// セキュリティイベントの保存
eventRepository.save(event);
// セキュリティログの出力
securityLogger.info("Security event: {}", event);
}
}
}
// セキュリティイベントの監視
@Service
public class SecurityMonitor {
private final SecurityEventRepository eventRepository;
private final AlertService alertService;
@Scheduled(fixedRate = 300000) // 5分ごと
public void monitorSecurityEvents() {
// ログイン失敗の監視
int failedLogins = countRecentFailedLogins(Duration.ofMinutes(5));
if (failedLogins > 10) {
alertService.sendAlert(
AlertLevel.WARNING,
"High number of failed login attempts detected",
Map.of("failed_attempts", failedLogins)
);
}
// 不正アクセスの検知
List<SecurityEvent> suspiciousEvents = detectSuspiciousActivity();
if (!suspiciousEvents.isEmpty()) {
alertService.sendAlert(
AlertLevel.HIGH,
"Suspicious activity detected",
Map.of("events", suspiciousEvents)
);
}
}
private List<SecurityEvent> detectSuspiciousActivity() {
return eventRepository.findAll(
where(SecurityEvent_.status).is(SecurityEventStatus.ACCESS_DENIED)
.and(SecurityEvent_.timestamp).greaterThan(Instant.now().minus(Duration.ofMinutes(5)))
.and(SecurityEvent_.ipAddress).in(getKnownMaliciousIPs())
);
}
}
4.2 アクセスログ
// アクセスログフィルターの実装例
@Component
public class AccessLogFilter implements Filter {
private final Logger accessLogger = LoggerFactory.getLogger("ACCESS");
private final ObjectMapper objectMapper;
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// リクエスト開始時刻
long startTime = System.currentTimeMillis();
try {
// リクエストの処理
chain.doFilter(request, response);
} finally {
// アクセスログの記録
AccessLogEntry entry = AccessLogEntry.builder()
.timestamp(Instant.now())
.method(httpRequest.getMethod())
.uri(httpRequest.getRequestURI())
.queryString(httpRequest.getQueryString())
.remoteAddress(httpRequest.getRemoteAddr())
.userAgent(httpRequest.getHeader("User-Agent"))
.referer(httpRequest.getHeader("Referer"))
.statusCode(httpResponse.getStatus())
.processingTime(System.currentTimeMillis() - startTime)
.build();
accessLogger.info(objectMapper.writeValueAsString(entry));
}
}
}
// アクセスログの分析
@Service
public class AccessLogAnalyzer {
private final ElasticsearchClient esClient;
public Map<String, Long> analyzeAccessPatterns(Duration period) {
// URIごとのアクセス数集計
SearchResponse<Void> response = esClient.search(s -> s
.index("access-logs-*")
.query(q -> q
.range(r -> r
.field("@timestamp")
.from(Instant.now().minus(period).toString())
.to(Instant.now().toString())
)
)
.aggregations("uri_stats", a -> a
.terms(t -> t
.field("uri")
.size(100)
)
),
Void.class
);
return processAccessStats(response);
}
public List<SlowRequest> analyzeSlowRequests(Duration period, long threshold) {
// 遅いリクエストの検索
SearchResponse<AccessLogEntry> response = esClient.search(s -> s
.index("access-logs-*")
.query(q -> q
.bool(b -> b
.must(m -> m
.range(r -> r
.field("@timestamp")
.from(Instant.now().minus(period).toString())
.to(Instant.now().toString())
)
)
.must(m -> m
.range(r -> r
.field("processing_time")
.gt(JsonData.of(threshold))
)
)
)
)
.sort(sort -> sort
.field(f -> f
.field("processing_time")
.order(SortOrder.Desc)
)
),
AccessLogEntry.class
);
return processSlowRequests(response);
}
}
5. 運用管理のベストプラクティス
5.1 ログローテーション
// ログローテーションの設定例
@Configuration
public class LogRotationConfig {
@Bean
public RollingFileAppender rollingFileAppender() {
RollingFileAppender appender = new RollingFileAppender();
// 基本設定
appender.setFile("/var/log/application.log");
appender.setAppend(true);
appender.setImmediateFlush(true);
// ローテーション設定
SizeAndTimeBasedRollingPolicy policy = new SizeAndTimeBasedRollingPolicy();
policy.setFileNamePattern("/var/log/application-%d{yyyy-MM-dd}-%i.log.gz");
policy.setMaxFileSize(FileSize.valueOf("100MB"));
policy.setMaxHistory(30);
policy.setTotalSizeCap(FileSize.valueOf("20GB"));
appender.setRollingPolicy(policy);
return appender;
}
}
// ログクリーンアップサービス
@Service
public class LogCleanupService {
private final Path logDirectory;
private final Duration retentionPeriod;
@Scheduled(cron = "0 0 1 * * *") // 毎日午前1時に実行
public void cleanupOldLogs() {
try {
Files.walk(logDirectory)
.filter(Files::isRegularFile)
.filter(this::isLogFile)
.filter(this::isOlderThanRetentionPeriod)
.forEach(this::deleteFile);
} catch (IOException e) {
logger.error("Failed to cleanup old log files", e);
}
}
private boolean isLogFile(Path path) {
return path.toString().endsWith(".log") ||
path.toString().endsWith(".log.gz");
}
private boolean isOlderThanRetentionPeriod(Path path) {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant()
.isBefore(Instant.now().minus(retentionPeriod));
} catch (IOException e) {
logger.warn("Failed to get last modified time for file: {}", path, e);
return false;
}
}
}
5.2 ログ監視とアラート
// ログ監視サービスの実装例
@Service
public class LogMonitoringService {
private final ElasticsearchClient esClient;
private final AlertService alertService;
private final MetricsRegistry metricsRegistry;
@Scheduled(fixedRate = 60000) // 1分ごとに実行
public void monitorLogs() {
// エラーログの監視
monitorErrorLogs();
// パフォーマンスの監視
monitorPerformance();
// セキュリティイベントの監視
monitorSecurityEvents();
// システムリソースの監視
monitorSystemResources();
}
private void monitorErrorLogs() {
// エラー数のカウント
SearchResponse<Void> response = esClient.search(s -> s
.index("logs-*")
.query(q -> q
.bool(b -> b
.must(m -> m
.range(r -> r
.field("@timestamp")
.from(Instant.now().minus(Duration.ofMinutes(5)).toString())
.to(Instant.now().toString())
)
)
.must(m -> m
.term(t -> t
.field("level")
.value("ERROR")
)
)
)
),
Void.class
);
long errorCount = response.hits().total().value();
// メトリクスの記録
metricsRegistry.counter("log.errors").increment(errorCount);
// アラートの評価
if (errorCount > 100) { // 5分間で100件以上のエラー
alertService.sendAlert(
AlertLevel.HIGH,
"High error rate detected",
Map.of("error_count", errorCount)
);
}
}
private void monitorPerformance() {
// 平均レスポンスタイムの計算
SearchResponse<Void> response = esClient.search(s -> s
.index("access-logs-*")
.query(q -> q
.range(r -> r
.field("@timestamp")
.from(Instant.now().minus(Duration.ofMinutes(5)).toString())
.to(Instant.now().toString())
)
)
.aggregations("avg_response_time", a -> a
.avg(avg -> avg
.field("response_time")
)
),
Void.class
);
double avgResponseTime = response.aggregations()
.get("avg_response_time")
.avg()
.value();
// メトリクスの記録
metricsRegistry.gauge("response.time.avg", avgResponseTime);
// アラートの評価
if (avgResponseTime > 1000) { // 1秒以上
alertService.sendAlert(
AlertLevel.WARNING,
"High average response time",
Map.of("avg_response_time", avgResponseTime)
);
}
}
}
// アラートサービス
@Service
public class AlertService {
private final NotificationService notificationService;
private final AlertRepository alertRepository;
public void sendAlert(AlertLevel level, String message, Map<String, Object> details) {
// アラートの作成
Alert alert = Alert.builder()
.timestamp(Instant.now())
.level(level)
.message(message)
.details(details)
.build();
// アラートの保存
alertRepository.save(alert);
// 通知の送信
NotificationRequest notification = NotificationRequest.builder()
.type(determineNotificationType(level))
.title(String.format("[%s] Alert: %s", level, message))
.content(createAlertContent(alert))
.recipients(determineRecipients(level))
.build();
notificationService.send(notification);
}
private NotificationType determineNotificationType(AlertLevel level) {
switch (level) {
case HIGH:
return NotificationType.SMS_AND_EMAIL;
case WARNING:
return NotificationType.EMAIL;
default:
return NotificationType.SLACK;
}
}
private List<String> determineRecipients(AlertLevel level) {
switch (level) {
case HIGH:
return Arrays.asList("oncall-team", "system-admin");
case WARNING:
return Arrays.asList("development-team");
default:
return Arrays.asList("monitoring-channel");
}
}
}
6. まとめ
効果的なログ設計は、システムの運用性と保守性を大きく向上させます。以下の点に注意を払いながら、適切なログ設計を行いましょう:
-
目的に応じたログレベルの使い分け
- エラーログ
- 監査ログ
- パフォーマンスログ
- セキュリティログ
-
構造化されたログフォーマット
- JSON形式の採用
- 必要な情報の網羅
- 一貫性のある形式
-
効果的なログ運用
- ログローテーション
- 集中管理
- 監視とアラート
参考文献
- 「実践ログ管理」- O’Reilly Japan
- 「Elastic Stack実践ガイド」- 技術評論社
- 「システム運用アーキテクチャ」- SoftwareDesign
- 「セキュリティログ分析入門」- インプレス
適切なログ設計は、システムの安定運用と問題解決の基盤となります。本記事で紹介したベストプラクティスを参考に、プロジェクトに適したログ設計を実践してください。
関連記事
2025/3/25
【「動作保証はどこまで?」SIerのためのシステム保守の基本】
SIerエンジニアのためのシステム保守ガイド。業務システムの保守範囲の定義から具体的な保守活動まで、実践的なアプローチを解説します。
2025/3/23
【長年運用されている業務システムの"負債"とどう向き合うか?】
SIerエンジニアのための技術的負債管理ガイド。長年運用されてきた業務システムの負債を理解し、効果的に管理・改善していくための実践的なアプローチを解説します。