Spring Batchのアプリ向けログ出力機能設計

cloud2 Spring Framework
cloud2

Spring Framework上で共通ログ機能の設計を行ってみます。ログ出力といっても色々な機能が考えられますが、今回はSpring Batch上で動作するアプリケーション向けに共通のログ出力インタフェースを提供することを考えてみます。

環境前提

Spring Bootv3.5.6
Java21
ログ出力ライブラリSLF4J + Logback
ビルドツールMaven

提供機能

ログ出力項目

以下のログ出力項目を出力する機能を作成します。アプリケーション側の実装では「業務固有メッセージ」だけを設定し、その他の項目は基盤部品側で自動で出力します。
任意の項目は項目が存在する場合のみ出力し、存在しない場合は「N/A」を出力します。

項目名フォーマット必須/任意内容項目設定元
タイムスタンプ%d{yyyy-MM-dd HH:mm:ss.SSS}必須ログ出力時刻Logback
ログレベル%-5level必須INFO / DEBUG / ERROR などLogback
ロガー名%logger必須ロガー名(パッケージ名は短縮形)Logback
ジョブ名%X{jobName}必須ジョブ論理名基盤
ジョブ実行ID%X{jobExecId}必須ジョブ実行ID基盤
ステップ名%X{stepName}任意ステップ論理名
※ステップ実行時のみ出力
基盤
ステップID%X{stepId}任意ステップID
※ステップ実行時のみ出力
基盤
業務固有メッセージ%X{msg}必須業務固有ログアプリ

設計方針

  • タイムスタンプなどLogbackで自動出力可能なものはLogbackで対応
  • 「ジョブ名」、「ジョブ実行ID」、「ステップ名」、「ステップID」は共通Listener内でMDCに設定
  • 共通Listenerを登録するためのヘルパークラスを作成
  • MDC操作、スレッド間コピーするためにMDC操作ユーティリティクラスを作成
  • ログはコンソールに1行テキストで出力
  • プロファイルはdev, prodなどをMavenで切り替え可能とする

Spring Boot/Logback/Maven設定

Logbackの設定

「logback-spring.xml」でに共通部品用のアペンダを追加しログ出力パターンを定義します。

<configuration>

  <!-- 共通プロパティ定義 -->
  <property name="LOG_CHARSET" value="UTF-8"/>
  <property name="LOG_PATTERN_COMMON"
          value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] %-5level [%logger{1}] [%X{jobName:-N/A}] [%thread] [%X{jobExecId:-N/A}] [%X{stepName:-N/A}] [%X{stepId:-N/A}] - %msg%n"/>

  <!-- 共通部品専用アペンダー -->
  <appender name="CONSOLE_COMMON" class="ch.qos.logback.core.ConsoleAppender">
    <target>System.out</target>
    <encoder>
      <pattern>${LOG_PATTERN_COMMON}</pattern>
      <charset>${LOG_CHARSET}</charset>
    </encoder>
  </appender>

  <!-- アプリケーション全体用アペンダー -->
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <target>System.out</target>
    <encoder>
      <pattern>${LOG_PATTERN_COMMON}</pattern>
      <charset>${LOG_CHARSET}</charset>
    </encoder>
  </appender>

  <!-- 共通部品専用ロガー -->
  <logger name="common.logging" level="INFO" additivity="false">
    <appender-ref ref="CONSOLE_COMMON"/>
  </logger>

  <!-- アプリケーション全体のルートロガー -->
  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>

</configuration>

ログ出力イメージ。

[2025-10-15 01:48:59.303] INFO  [c.logging] [helloWorldJob] [restartedMain] [1] [helloWorldStep] [1] - ★★★アプリケーションの独自ログ★★★

Springアプリケーション設定ファイル

開発、本番などでプロファイルの切り替えが出来るようにactiveプロパティを定義します。設定値はMavenから「env.profile」という名前で受け取ります。

spring:
  application:
    name: BatchHelloWorldTasklet

  profiles:
    active: "@env.profile@"  # ← filtering対象

開発環境用の設定ファイルを用意します。設定内容は実際には利用しない適当なものを指定しています。

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    password:

Mavenプロジェクト定義ファイル

Mavenでプロファイルを切り替えるためにpom.xmlのプロパティに「env.profile」を指定します。ここでは開発用プロファイルを利用する想定で”dev”を指定しています。

	<properties>
	  <env.profile>dev</env.profile> <!-- 環境を指定 -->
	</properties>

application.ymlの「@spring.profiles.active@」を置換するためにフィルタリングを有効にします。

<build>
  <resources>
    <resource>
      <directory>src/main/resources</directory>
      <filtering>true</filtering>
    </resource>
  </resources>
</build>

アプリケーション向けの共通ログ出力インターフェース実装

MDCコンテキスト操作用ユーティリティ

共通ログ出力インタフェースから利用する、MDCコンテキスト操作を行うユーティリティクラスを作成します。スレッド間でMDCをコピーして共有するための「wrapWithMDCContext」を提供します。

package com.example.demo.common.logging;

import java.util.Collections;
import java.util.Map;

import org.slf4j.MDC;

/**
 * MDCコンテキストの操作を共通化するユーティリティクラス。
 */
public final class LoggingContextUtil {

    private LoggingContextUtil() {
        // インスタンス化防止
    }

    /**
     * 現在のMDCコンテキストを取得
     */
    public static Map<String, String> capture() {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return context != null ? context : Collections.emptyMap();
    }

    /**
     * 指定されたコンテキストをMDCに設定
     */
    public static void restore(Map<String, String> context) {
        MDC.clear();
        if (context != null) {
            MDC.setContextMap(context);
        }
    }

    /**
     * MDCをクリア(明示的な終了処理)
     */
    public static void clear() {
        MDC.clear();
    }

    /**
     * 現在のMDCコンテキストを指定された非同期タスクにコピーする
     * 非同期処理(ExecutorService や CompletableFuture など)で、
     * スレッドが切り替わり、MDCの内容が引き継がれない場合に利用。
     */
    public static Runnable wrapWithMDCContext(Runnable task) {
        Map<String, String> context = capture();
        return () -> {
            restore(context);
            try {
                task.run();
            } finally {
                clear();
            }
        };
    }
}

アプリケーション向け共通ログインタフェース

アプリケーション向けの共通ログ出力インターフェースを作成します。

package com.example.demo.common.logging;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * アプリケーション向けの共通ログ出力インターフェース。
 */
public final class CommonLogger {

	private static final String LOGGER_NAME = "common.logging";
    private static final Logger logger = LoggerFactory.getLogger(LOGGER_NAME);
    
    private CommonLogger() {
        // インスタンス化防止
    }

    // ─────────────────────────────────────────────
    // アプリケーション向けログインタフェース
    // ─────────────────────────────────────────────

    public static void info(String message) {
        logger.info(message);
    }

    public static void debug(String message) {
        logger.debug(message);
    }

    public static void warn(String message) {
        logger.warn(message);
    }

    public static void error(String message, Throwable throwable) {
        logger.error(message, throwable);
    }

    // ─────────────────────────────────────────────
    // MDCコンテキスト操作
    // ─────────────────────────────────────────────

    public static void restoreContext(Map<String, String> context) {
        LoggingContextUtil.restore(context);
    }

    public static void clearContext() {
        LoggingContextUtil.clear();
    }


    /**
     * 現在のMDCコンテキストを非同期タスクにコピーする。
     * <p>
     * 非同期処理(ExecutorServiceやCompletableFutureなど)で使用することで、
     * スレッド切り替えによるMDCの消失を防ぎます。
     * <p>
	 * 使用例:
	 * <pre>{@code
	 * MDC.put("XXX", "123");
	 * Runnable task = CommonLogger.wrapWithMDCContext(() -> {
	 *     CommonLogger.info("在庫更新処理を開始.");
	 * });
	 * new Thread(task).start();
	 * }</pre>
     *
     * @param task 実行したい処理(Runnable)
     * @return MDCコンテキストを注入したRunnable
     */
    public static Runnable wrapWithMDCContext(Runnable task) {
        return LoggingContextUtil.wrapWithMDCContext(task);
    }
}

共通ログ項目出力用Listener

共通ログ項目をMDCに設定するListenerを準備します。(ジョブ用とステップ用)

package com.example.demo.common.job.listener;

import org.slf4j.MDC;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.stereotype.Component;

@Component
public class LoggingContextJobListener implements JobExecutionListener {

    private static final String MDC_KEY_JOB_NAME    = "jobName";
    private static final String MDC_KEY_JOB_EXEC_ID = "jobExecId";

    @Override
    public void beforeJob(JobExecution jobExecution) {
        MDC.put(MDC_KEY_JOB_NAME, jobExecution.getJobInstance().getJobName());
        MDC.put(MDC_KEY_JOB_EXEC_ID, String.valueOf(jobExecution.getId()));
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        MDC.remove(MDC_KEY_JOB_NAME);
        MDC.remove(MDC_KEY_JOB_EXEC_ID);
    }
}
package com.example.demo.common.job.listener;

import org.slf4j.MDC;
import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepExecutionListener;
import org.springframework.stereotype.Component;

@Component
public class LoggingContextStepListener implements StepExecutionListener {

    private static final String MDC_KEY_STEP_NAME   = "stepName";
    private static final String MDC_KEY_STEP_ID     = "stepId";
    private static final String MDC_KEY_JOB_EXEC_ID = "jobExecId";

    @Override
    public void beforeStep(StepExecution stepExecution) {
        MDC.put(MDC_KEY_STEP_NAME, stepExecution.getStepName());
        MDC.put(MDC_KEY_STEP_ID, String.valueOf(stepExecution.getId()));
        MDC.put(MDC_KEY_JOB_EXEC_ID, String.valueOf(stepExecution.getJobExecution().getId()));
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        MDC.remove(MDC_KEY_STEP_NAME);
        MDC.remove(MDC_KEY_STEP_ID);
        MDC.remove(MDC_KEY_JOB_EXEC_ID);
        return stepExecution.getExitStatus();
    }
}

BatchConfig用のヘルパー

共通ログ項目出力用Listenerはすべてのジョブとステップに登録するのでヘルパークラスを用意します。このクラスはBatchConfigから利用します。

package com.example.demo.common.job.helper;

import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;

import com.example.demo.common.job.listener.LoggingContextJobListener;
import com.example.demo.common.job.listener.LoggingContextStepListener;

@Component
public class BatchBuilderHelper {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final LoggingContextJobListener jobListener;
    private final LoggingContextStepListener stepListener;

    public BatchBuilderHelper(JobRepository jobRepository,
                              PlatformTransactionManager transactionManager,
                              LoggingContextJobListener jobListener,
                              LoggingContextStepListener stepListener) {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
        this.jobListener = jobListener;
        this.stepListener = stepListener;
    }

    public JobBuilder getJobBuilder(String jobName) {
    	//共通Listenerをセット
        return new JobBuilder(jobName, jobRepository).listener(jobListener);
    }

    public StepBuilder getStepBuilder(String stepName) {
    	//共通Listenerをセット
        return new StepBuilder(stepName, jobRepository).listener(stepListener);
    }

    public PlatformTransactionManager getTransactionManager() {
        return transactionManager;
    }
}

アプリケーション側の実装サンプル

BatchConfigにジョブとステップを定義します。共通ログ出力用のListener登録を行うためにヘルパークラスを利用しています。

package com.example.demo.config;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.demo.common.job.helper.BatchBuilderHelper;
import com.example.demo.tasklet.HelloTasklet;

@Configuration
public class BatchConfig {

    @Bean
    public Job helloWorldJob(BatchBuilderHelper helper, Step helloWorldStep) {
        return helper.getJobBuilder("helloWorldJob")
                     .start(helloWorldStep)
                     .build();
    }

    @Bean
    public Step helloWorldStep(BatchBuilderHelper helper, HelloTasklet helloTasklet) {
        return helper.getStepBuilder("helloWorldStep")
                     .tasklet(helloTasklet, helper.getTransactionManager())
                     .build();
    }
}

タスクレット定義です。ログだけを出力する単純なタスクレットです。

package com.example.demo.tasklet;

import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.stereotype.Component;

import com.example.demo.common.logging.CommonLogger;

@Component
@StepScope
public class HelloTasklet implements Tasklet {

    @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
    	CommonLogger.info("★★★アプリケーションの独自ログ★★★");
        return RepeatStatus.FINISHED;
    }
}

その他

・MDCに設定するキーは個々のクラスで定数化していますが、MDC設定用クラスに分離したほうが重複の危険性がなくなるのでより良いと思います。
・アペンダー、ロガーは共通で1つ用意していますが、実際にはWebアプリ用、バッチ用などに分けて定義が必要。

以上

タイトルとURLをコピーしました