CLOVER🍀

That was when it all began.

LogbackLogstash Logback Encoderでログ内の文字列を眮換する

これは、なにをしたくお曞いたもの

Logbackを䜿っおログ出力をする時に、ログメッセヌゞを眮換する方法はないかなずいうこずで調べおみたした。

以䞋の方法がありそうです。

  • replace倉換指定子を䜿う
  • 自分でConverterを䜜成する
  • MaskingJsonGeneratorDecoratorを䜿うLogstash Logback Encoder

これを、通垞のLogbackをLogstash Logback Encoderを䜿った時それぞれで調べおみたした。

ログ出力する際に文字列眮換する

Logbackでログ出力する際には、replace倉換指定子Conversion specifierを䜿うのが良さそうです。

Layouts / PatternLayout

こちらですね。

Layouts / PatternLayout / replace

こちらに䜿い方の䟋が茉っおいたす。

Layouts / Conversion word options

%replaceの()内に察象の文字列を、{}内に正芏衚珟ず眮換埌の文字列を䞎えるようです。

<pattern>%-5level - %replace(%msg){'\d{14,16}', 'XXXX'}%n</pattern>

今たであんたり気にしおいなかったのですが、そもそも%msgのようなものを「Conversion word」ず呌ぶみたいですね。

PatternLayoutの箇所にConversion wordの䞀芧が茉っおいたす。

Layouts / PatternLayout

そしお、倉換指定子は自分でも䜜成するこずができたす。

Layouts / Evaluators / Creating a custom conversion specifier

ドキュメントには、ClassicConverterクラスを継承しお䜜成する䟋が曞かれおいたす。

ClassicConverter (Logback-Parent 1.3.0 API)

他にも、CompositeConverterクラスを継承しお䜜成する方法もありたす。

CompositeConverter (Logback-Parent 1.3.0 API)

Conversion wordは、オプションを取るこずができたす。スタックトレヌスを衚珟する%ex{short}などがいい䟋ですよね。
これはClassicConverterクラスのサブクラスずしお実珟されおいたす。

replace倉換指定子は、さらに()で察象の文字列を指定するこずができたす。こんな感じで。

`%replace(%msg){'\d{14,16}', 'XXXX'}`

これはCompositeConverterクラスのサブクラスずしお実珟されおいたす。

Mask Sensitive Data in Logs With Logback | Baeldung

たずえば%msgの実䜓はこちらです。

https://github.com/qos-ch/logback/blob/v_1.4.7/logback-classic/src/main/java/ch/qos/logback/classic/pattern/MessageConverter.java

replace倉換指定子の実䜓はこちらです。

https://github.com/qos-ch/logback/blob/v_1.4.7/logback-core/src/main/java/ch/qos/logback/core/pattern/ReplacingCompositeConverter.java

このあたりの指定子は、以䞋のパッケヌゞを芋るずよいでしょう。

https://github.com/qos-ch/logback/tree/v_1.4.7/logback-core/src/main/java/ch/qos/logback/core/pattern

https://github.com/qos-ch/logback/tree/v_1.4.7/logback-classic/src/main/java/ch/qos/logback/classic/pattern

PatternLayoutには、これらがデフォルトで登録されおいるので各皮指定子が䜿える、ずいうこずになりたす。

https://github.com/qos-ch/logback/blob/v_1.4.7/logback-classic/src/main/java/ch/qos/logback/classic/PatternLayout.java#L52-L162

https://github.com/qos-ch/logback/blob/v_1.4.7/logback-core/src/main/java/ch/qos/logback/core/pattern/parser/Parser.java#L47-L53

倉換指定子を自分で䜜成した堎合は、以䞋のようにconversionRuleで登録したす。

<configuration>

  <conversionRule conversionWord="nanos"
                  converterClass="layouts.MySampleConverter" />

  <appender name="STDOUT" class="ch.qos.logback.;core.ConsoleAppender">
    <encoder>
      <pattern>%-6nanos [%thread] -%kvp -%msg%n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Layouts / Evaluators / Creating a custom conversion specifier

Spring Bootでも䜿われおいるようですね。

https://github.com/spring-projects/spring-boot/blob/v3.0.6/spring-boot-project/spring-boot/src/main/resources/org/springframework/boot/logging/logback/defaults.xml

たずはこのあたりを詊しおみたしょう。
※Logstash Logback Encoderは埌で扱いたす

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment (build 17.0.6+10-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.6+10-Ubuntu-0ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.1 (2e178502fcdbffc201671fb2537d0cb4b4cc58f8)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.6, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-70-generic", arch: "amd64", family: "unix"

準備

Maven䟝存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.7</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.7</version>
        </dependency>
    </dependencies>

たずはSLF4Jずlogback-classicを䟝存関係に加えたす。

なお、SLF4JずLogbackのバヌゞョン関係は以䞋のようです。必芁なJavaのバヌゞョンや、Java EEJakarta EEのサポヌトにも
差がありたすね。Logback 1.3ず1.4の差はそれくらいのようです。

News

お題

文字列眮換ずいうこずで、今回はログメッセヌゞ内にメヌルアドレスが出珟したら別の文字列に眮き換える、ずいうお題で考えたいず
思いたす。

サンプルプログラム

サンプルプログラムずしおは、以䞋のようなものを甚意。

sc/main/java/org/littlewings/logback/App.java

package org.littlewings.logback;

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

public class App {
    public static void main(String... args) {
        Logger logger = LoggerFactory.getLogger(App.class);

        logger.info("Hello World!!");
        logger.info("Hello, isono@example.com, namino@example.com !!");
        logger.info("Hello, {}, {} !!", "isono@example.com", "namino@example.net");

        try {
            throwException("Oops!! namino@example.net");
        } catch (RuntimeException e) {
            logger.info("Hello, {} !!", "isono@example.com", new RuntimeException(e));
        }
    }

    static void throwException(String message) {
        throw new RuntimeException(message);
    }
}

ログ䞭にメヌルアドレスが入ったり、眮換文字列{}内にメヌルアドレスを入れおみたり、䟋倖メッセヌゞにメヌルアドレスを
入れおみたり。

最初のlogback.xmlは、こんな感じでいきたす。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

実行結果。

2023-04-23 16:13:09.923 [main] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:13:09.925 [main] INFO  org.littlewings.logback.App - Hello, isono@example.com, namino@example.com !!
2023-04-23 16:13:09.925 [main] INFO  org.littlewings.logback.App - Hello, isono@example.com, namino@example.net !!
2023-04-23 16:13:09.926 [main] INFO  org.littlewings.logback.App - Hello, isono@example.com !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! namino@example.net
    at org.littlewings.logback.App.main(App.java:17)
Caused by: java.lang.RuntimeException: Oops!! namino@example.net
    at org.littlewings.logback.App.throwException(App.java:22)
    at org.littlewings.logback.App.main(App.java:15)

ここからいろいろ倉えおいこうず思いたす。

Logbackでログ内の文字列を眮換する

replace倉換指定子を䜿う

最初に䜿うのは、replace倉換指定子。

Layouts / PatternLayout / replace

logback.xmlは、以䞋のように倉曎。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'\w+@\w+\.\w+', 'xxxxx@xxxxx'}%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

%msg倉換指定子を%replace(){'正芏衚珟', '眮換埌の文字列}で囲みたす。

%replace(%msg){'\w+@\w+\.\w+', 'xxxxx@xxxxx'}

結果。

2023-04-23 16:17:22.211 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:17:22.213 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, xxxxx@xxxxx, xxxxx@xxxxx !!
2023-04-23 16:17:22.213 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, xxxxx@xxxxx, xxxxx@xxxxx !!
2023-04-23 16:17:22.214 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, xxxxx@xxxxx !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! namino@example.net
        at org.littlewings.logback.App.main(App.java:17)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.RuntimeException: Oops!! namino@example.net
        at org.littlewings.logback.App.throwException(App.java:22)
        at org.littlewings.logback.App.main(App.java:15)
        ... 2 common frames omitted

䟋倖の郚分が倉わっおいたせんね。では、こうしおみたしょう。

      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %replace(%msg){'\w+@\w+\.\w+', 'xxxxx@xxxxx'}%n%replace(%ex){'\w+@\w+\.\w+', 'xxxxx@xxxxx'}</pattern>

%exを明瀺的に远加しお、こちらを眮換するようにしおみたした。

今床はうたくいきたす。

2023-04-23 16:18:28.674 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:18:28.676 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, xxxxx@xxxxx, xxxxx@xxxxx !!
2023-04-23 16:18:28.676 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, xxxxx@xxxxx, xxxxx@xxxxx !!
2023-04-23 16:18:28.677 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, xxxxx@xxxxx !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! xxxxx@xxxxx
        at org.littlewings.logback.App.main(App.java:17)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.RuntimeException: Oops!! xxxxx@xxxxx
        at org.littlewings.logback.App.throwException(App.java:22)
        at org.littlewings.logback.App.main(App.java:15)
        ... 2 common frames omitted

ただ、この方法だず正芏衚珟を蚭定ファむル内に曞くこずになりたす。今回のように簡単な正芏衚珟であればよいですが、たずえば
WHATWGの定矩する正芏衚珟のようなものだったりするずちょっず面倒な感じがしたす。

The following JavaScript- and Perl-compatible regular expression is an implementation of the above definition.

/^[a-zA-Z0-9.!#$%&'+\/=?^_`{|}~-]+@a-zA-Z0-9?(?:.a-zA-Z0-9?)$/

HTML / The elements of HTML / Forms / The input element / States of the type attribute / Email state / valid email address

倉換内容をJavaコヌド偎で衚珟できたらよいな、ずいうこずで、別の仕組みも詊しおみたす。

ClassicConverterを䜿う

次は、ClassicConverterを䜿っおみたしょう。

ClassicConverter (Logback-Parent 1.3.0 API)

ClassicConverterを䜿うずいうこずは、倉換指定子を自䜜するずいうこずですね。

Layouts / Evaluators / Creating a custom conversion specifier

ドキュメントの䟋では、%nanosずいう倉換指定子を䜜成しおいたす。

今回は、以䞋のようなClassicConverterのサブクラスを䜜成。

src/main/java/org/littlewings/logback/MyMessageConverter.java

package org.littlewings.logback;

import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MyMessageConverter extends ClassicConverter {
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
            "[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*"
    );

    @Override
    public String convert(ILoggingEvent event) {
        //String message = event.getMessage();  // {}のたた
        String message = event.getFormattedMessage();  // {}を解決した埌

        Matcher matcher = EMAIL_PATTERN.matcher(message);

        if (matcher.find()) {
            return matcher.replaceAll("[xxxxx]");
        }

        return message;
    }
}

ILoggingEventから取埗できるメッセヌゞを正芏衚珟で眮換しおみたす。

これを%mymsgずしおconversionRuleに登録。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <conversionRule conversionWord="mymsg" converterClass="org.littlewings.logback.MyMessageConverter" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mymsg%n</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

pattern内で䜿っおみたす。

      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mymsg%n</pattern>

結果。

2023-04-23 16:31:26.018 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:31:26.021 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx], [xxxxx] !!
2023-04-23 16:31:26.021 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx], [xxxxx] !!
2023-04-23 16:31:26.022 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx] !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! namino@example.net
        at org.littlewings.logback.App.main(App.java:17)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.RuntimeException: Oops!! namino@example.net
        at org.littlewings.logback.App.throwException(App.java:22)
        at org.littlewings.logback.App.main(App.java:15)
        ... 2 common frames omitted

あくたでメッセヌゞを眮換しおいるだけなので、この方法だず䟋倖メッセヌゞは手が出たせんね 。

ちなみに、以䞋の郚分を入れ替えるず{}を評䟡する前のメッセヌゞを扱うこずになるので

    @Override
    public String convert(ILoggingEvent event) {
        String message = event.getMessage();  // {}のたた
        //String message = event.getFormattedMessage();  // {}を解決した埌

結果がこうなりたす。

2023-04-23 16:32:24.751 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:32:24.754 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx], [xxxxx] !!
2023-04-23 16:32:24.754 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, {}, {} !!
2023-04-23 16:32:24.755 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, {} !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! namino@example.net
        at org.littlewings.logback.App.main(App.java:17)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.RuntimeException: Oops!! namino@example.net
        at org.littlewings.logback.App.throwException(App.java:22)
        at org.littlewings.logback.App.main(App.java:15)
        ... 2 common frames omitted

こんな䜿い方はしないずは思いたすが 。

CompositeConverterを䜿う

次は、CompositeConverterクラスを䜿っおみたす。こちらはドキュメントには蚘茉がありたせんが、%replace倉換指定子はこちらで
実装されおいたす。

CompositeConverter (Logback-Parent 1.3.0 API)

ClassicConverterずの違いは、別の倉換指定子などを入力にできるこずですね。

たずえば、先ほど%replaceを䜿っお䟋ではこんな感じで%msgを匕数にしおいたした。

%replace(%msg){'\w+@\w+\.\w+', 'xxxxx@xxxxx'}

ちなみにClassicConverterでも{}は䜿えたす先ほどのサンプルでは扱いたせんでした。

こう考えるず、以䞋のようなものもClassicConverterにオプションずしお枡しおいるこずがわかりたす。

%logger{36}
%X{user}

MDCも倉換指定子で実珟されおいたす。

で、今回はこんなCompositeConverterを䜜成。

src/main/java/org/littlewings/logback/EmailReplaceConverter.java

package org.littlewings.logback;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.pattern.CompositeConverter;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class EmailReplaceConverter extends CompositeConverter<ILoggingEvent> {
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
            "[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*"
    );

    String replacement;

    @Override
    public void start() {
        replacement = getFirstOption() != null ? getFirstOption() : "[xxxxx]";
    }

    @Override
    protected String transform(ILoggingEvent event, String in) {
        Matcher matcher = EMAIL_PATTERN.matcher(in);

        if (matcher.find()) {
            return matcher.replaceAll(replacement);
        }

        return in;
    }
}

transformメ゜ッドの第2匕数に、()で指定した倀が枡っおきたす。

オプションも䞎え、指定されおいなかったら[xxxxx]ずするようにしおみたした。
※この蚘述はClassicConverterでも可胜です

    @Override
    public void start() {
        replacement = getFirstOption() != null ? getFirstOption() : "[xxxxx]";
    }

これをconversionRuleに%emailで倉換指定子ずしお扱えるように登録。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <conversionRule conversionWord="email" converterClass="org.littlewings.logback.EmailReplaceConverter" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %email(%msg){}%n%email(%ex){}</pattern>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

メッセヌゞずスタックトレヌスを倉換できるようにしたした。

      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %email(%msg){}%n%email(%ex){}</pattern>

結果。

機胜しおいるようです。

2023-04-23 16:44:51.020 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:44:51.022 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx], [xxxxx] !!
2023-04-23 16:44:51.022 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx], [xxxxx] !!
2023-04-23 16:44:51.024 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [xxxxx] !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! [xxxxx]
        at org.littlewings.logback.App.main(App.java:17)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.RuntimeException: Oops!! [xxxxx]
        at org.littlewings.logback.App.throwException(App.java:22)
        at org.littlewings.logback.App.main(App.java:15)
        ... 2 common frames omitted

オプションを指定しおみたしょう。

      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %email(%msg){'[zzzzz]'}%n%email(%ex){'[zzzzz]'}</pattern>

反映されたしたね。

2023-04-23 16:46:42.657 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello World!!
2023-04-23 16:46:42.659 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [zzzzz], [zzzzz] !!
2023-04-23 16:46:42.659 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [zzzzz], [zzzzz] !!
2023-04-23 16:46:42.661 [org.littlewings.logback.App.main()] INFO  org.littlewings.logback.App - Hello, [zzzzz] !!
java.lang.RuntimeException: java.lang.RuntimeException: Oops!! [zzzzz]
        at org.littlewings.logback.App.main(App.java:17)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)
        at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.RuntimeException: Oops!! [zzzzz]
        at org.littlewings.logback.App.throwException(App.java:22)
        at org.littlewings.logback.App.main(App.java:15)
        ... 2 common frames omitted

Logback単䜓だずこんなずころでしょうか。

Logstash Logback Encoderを䜿う

続いおは、Logstash Logback Encoderを䜿った堎合をテヌマにしたす。

ここから先は、䟝存関係にlogstash-logback-encoderを加えおいるものずしたす。

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.7</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.4.7</version>
        </dependency>
        <dependency>
            <groupId>net.logstash.logback</groupId>
            <artifactId>logstash-logback-encoder</artifactId>
            <version>7.3</version>
        </dependency>
    </dependencies>

たた、以䞋の゜ヌスコヌドは特に倉曎したせん。

src/main/java/org/littlewings/logback/App.java

package org.littlewings.logback;

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

public class App {
    public static void main(String... args) {
        Logger logger = LoggerFactory.getLogger(App.class);

        logger.info("Hello World!!");
        logger.info("Hello, isono@example.com, namino@example.com !!");
        logger.info("Hello, {}, {} !!", "isono@example.com", "namino@example.net");

        try {
            throwException("Oops!! namino@example.net");
        } catch (RuntimeException e) {
            logger.info("Hello, {} !!", "isono@example.com", new RuntimeException(e));
        }
    }

    static void throwException(String message) {
        throw new RuntimeException(message);
    }
}
Masking

Logstash Logback Encoderにはマスキングの機胜がありたす。

Logstash Logback Encoder / Masking

甚途ずしおは、個人情報や機密情報のマスキングに䜿うこずを想定されおいるようです。

The MaskingJsonGeneratorDecorator can be used to mask sensitive values (e.g. personally identifiable information (PII) or financial data).

マスキングの機胜を䜿うず、特定のパスフィヌルドをマスキングしたり、倀を正芏衚珟でマスキングするこずができるようになりたす。

たずはこちらを䜿っおみたす。

簡単に蚭定。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
        <defaultMask>****</defaultMask>

        <path>message</path>
        <path>logger_name</path>
      </jsonGeneratorDecorator>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

MaskingJsonGeneratorDecoratorを䜿いたす。

defaultMaskでマスキングした時のデフォルト倀を指定できたすが、これを省略するず****を指定したこずになりたす。

        <defaultMask>****</defaultMask>

最初はmessageずstack_traceを指定。

        <path>message</path>
        <path>logger_name</path>

結果。指定したフィヌルドがマスキングされたした。

{"@timestamp":"2023-04-23T17:01:27.002749969+09:00","@version":"1","message":"****","logger_name":"****","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:01:27.015472912+09:00","@version":"1","message":"****","logger_name":"****","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:01:27.015804802+09:00","@version":"1","message":"****","logger_name":"****","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:01:27.016798701+09:00","@version":"1","message":"****","logger_name":"****","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000,"stack_trace":"java.lang.RuntimeException: java.lang.RuntimeException: Oops!! namino@example.net\n\tat org.littlewings.logback.App.main(App.java:17)\n\tat org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\nCaused by: java.lang.RuntimeException: Oops!! namino@example.net\n\tat org.littlewings.logback.App.throwException(App.java:22)\n\tat org.littlewings.logback.App.main(App.java:15)\n\t... 2 common frames omitted\n"}

他にもパスでの指定方法は皮類があるので、詳しくはドキュメントを参照。

Logstash Logback Encoder / Masking / dentifying field values to mask by path

次は、倀でマスキングしおみたす。

Identifying field values to mask by value

ログ内に出珟する倀を正芏衚珟でマスキングできたすが、パス指定のマスキングよりも実行コストがかかるこずが泚意点です。

Identifying data to mask by value is much more expensive than identifying data to mask by path. Therefore, prefer identifying data to mask by path.

たた、倀のマスキングは定矩された数だけ順次実行されおいきたすが、この評䟡順には䟝存すべきではないこずが曞かれおいたす。

The value to mask is passed through every value masker, with the output of one masker passed as input to the next masker. This allows each masker to mask specific substrings within the value. The order in which the maskers are executed is not defined, and should not be relied upon.

今回は、シンプルに蚭定。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
        <defaultMask>****</defaultMask>

        <value>\w+@\w+\.\w+</value>
      </jsonGeneratorDecorator>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

結果。

{"@timestamp":"2023-04-23T17:07:54.178933736+09:00","@version":"1","message":"Hello World!!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:07:54.187134752+09:00","@version":"1","message":"Hello, ****, **** !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:07:54.189851624+09:00","@version":"1","message":"Hello, ****, **** !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:07:54.191020147+09:00","@version":"1","message":"Hello, **** !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000,"stack_trace":"java.lang.RuntimeException: java.lang.RuntimeException: Oops!! ****\n\tat org.littlewings.logback.App.main(App.java:17)\n\tat org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\nCaused by: java.lang.RuntimeException: Oops!! ****\n\tat org.littlewings.logback.App.throwException(App.java:22)\n\tat org.littlewings.logback.App.main(App.java:15)\n\t... 2 common frames omitted\n"}

メヌルアドレスの郚分がマスキングされたした。

マスキング凊理をクラスでも実装するこずができたす。ValueMaskerずいうむンタヌフェヌスを実装したす。

src/main/java/org/littlewings/logback/EmailValueMasker.java

package org.littlewings.logback;

import com.fasterxml.jackson.core.JsonStreamContext;
import net.logstash.logback.mask.ValueMasker;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class EmailValueMasker implements ValueMasker {
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
                "[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*"
        );

    @Override
    public Object mask(JsonStreamContext context, Object value) {
        if (value instanceof String in) {
            Matcher matcher = EMAIL_PATTERN.matcher(in);

            if (matcher.find()) {
                return matcher.replaceAll("[*****]");
            }
        }

        return null;
    }
}

䜜成したクラスは、valueMaskerで蚭定。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder">
      <jsonGeneratorDecorator class="net.logstash.logback.mask.MaskingJsonGeneratorDecorator">
        <defaultMask>****</defaultMask>

        <valueMasker class="org.littlewings.logback.EmailValueMasker" />
      </jsonGeneratorDecorator>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

結果。

{"@timestamp":"2023-04-23T17:16:09.271622188+09:00","@version":"1","message":"Hello World!!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:16:09.280521883+09:00","@version":"1","message":"Hello, [*****], [*****] !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:16:09.283502453+09:00","@version":"1","message":"Hello, [*****], [*****] !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000}
{"@timestamp":"2023-04-23T17:16:09.284884527+09:00","@version":"1","message":"Hello, [*****] !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","level_value":20000,"stack_trace":"java.lang.RuntimeException: java.lang.RuntimeException: Oops!! [*****]\n\tat org.littlewings.logback.App.main(App.java:17)\n\tat org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\nCaused by: java.lang.RuntimeException: Oops!! [*****]\n\tat org.littlewings.logback.App.throwException(App.java:22)\n\tat org.littlewings.logback.App.main(App.java:15)\n\t... 2 common frames omitted\n"}

OKですね。

倀でのマスキングの指定方法は、他にもいく぀かありたすが詳しくはドキュメントを参照、ずいうこずで。

Identifying field values to mask by value

倉換指定子を䜿う

Logstash Logback Encoderでも倉換指定子を䜿うこずができたす。

ただ、指定方法がちょっずプリミティブになりたす。

これたでencoderにLogstashEncoderを指定しおいたした。

    <encoder class="net.logstash.logback.encoder.LogstashEncoder">

今回はLoggingEventCompositeJsonEncoderを指定するこずになりたす。

    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">

LoggingEventCompositeJsonEncoderは、LogstashEncoderの芪クラスです。
LogstashEncoderはProviderず呌ばれる出力芁玠をデフォルトで蚭定したものであり、LoggingEventCompositeJsonEncoderを䜿うず
デフォルト蚭定はなくなりたすが、その代わりに柔軟なカスタマむズができるようになりたす。

Logstash Logback Encoder / Composite Encoder/Layout

たずえば、encoderにLoggingEventCompositeJsonEncoderのみを蚭定しおみたす。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

するず、ログになにも含たれなくなりたす。

{}
{}
{}
{}

この時、Logstash Logback EncoderからはProviderが蚭定されおいないずERRORが出おいたりしたす。

22:42:26,570 |-ERROR in net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder@7f977055 - No providers configured

LogstashEncoder盞圓の蚭定をしようずするず、このくらいでしょうか。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <version/>
        <message />
        <loggerName />
        <threadName />
        <logLevel />
        <stackTrace />
        <context />
        <mdc/>
        <tags />
        <arguments />
      </providers>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

https://github.com/logfellow/logstash-logback-encoder/blob/logstash-logback-encoder-7.3/src/main/java/net/logstash/logback/encoder/LogstashEncoder.java#L33

https://github.com/logfellow/logstash-logback-encoder/blob/logstash-logback-encoder-7.3/src/main/java/net/logstash/logback/LogstashFormatter.java#L115-L129

結果。

{"@timestamp":"2023-04-23T22:46:39.340368538+09:00","@version":"1","message":"Hello World!!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO"}
{"@timestamp":"2023-04-23T22:46:39.346415333+09:00","@version":"1","message":"Hello, isono@example.com, namino@example.com !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO"}
{"@timestamp":"2023-04-23T22:46:39.346623562+09:00","@version":"1","message":"Hello, isono@example.com, namino@example.net !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO"}
{"@timestamp":"2023-04-23T22:46:39.347528225+09:00","@version":"1","message":"Hello, isono@example.com !!","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","stack_trace":"java.lang.RuntimeException: java.lang.RuntimeException: Oops!! namino@example.net\n\tat org.littlewings.logback.App.main(App.java:17)\n\tat org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\nCaused by: java.lang.RuntimeException: Oops!! namino@example.net\n\tat org.littlewings.logback.App.throwException(App.java:22)\n\tat org.littlewings.logback.App.main(App.java:15)\n\t... 2 common frames omitted\n"}

このように、Providerを远加するにしたがっおログに出力される項目が増えおいきたす。

指定できるProviderは、このあたりに蚘茉がありたす。

Logstash Logback Encoder / Composite Encoder/Layout / Providers common to LoggingEvents and AccessEvents

Logstash Logback Encoder / Composite Encoder/Layout / Providers for LoggingEvents

ここにさらにpatternpatternを加えるこずで、出力するログに項目を「远加」できたす。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <version/>
        <!-- <message /> -->
        <loggerName />
        <threadName />
        <logLevel />
        <!-- <stackTrace /> -->
        <context />
        <mdc/>
        <tags />
        <arguments />
        <pattern>
          <pattern>
            {
              "message": "%msg",
              "stack_trace": "%ex"
            }
          </pattern>
        </pattern>
      </providers>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

今回はmessageずstack_traceを远加したので、Providerの方はコメントアりトしおいたす。これを残したたたにするず、同じ内容が
二重に远加されるこずになりたす。

ではここで、%replace倉換指定子を远加しおみたす。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <version/>
        <!-- <message /> -->
        <loggerName />
        <threadName />
        <logLevel />
        <!-- <stackTrace /> -->
        <context />
        <mdc/>
        <tags />
        <arguments />
        <pattern>
          <pattern>
            {
              "message": "%replace(%msg){'\\w+@\\w+\\.\\w+', 'xxxxx@xxxxx'}",
              "stack_trace": "%replace(%ex){'\\w+@\\w+\\.\\w+', 'xxxxx@xxxxx'}"
            }
          </pattern>
        </pattern>
      </providers>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

ちょっず゚スケヌプが面倒ですね 。

結果。

{"@timestamp":"2023-04-23T23:00:54.515349092+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello World!!","stack_trace":""}
{"@timestamp":"2023-04-23T23:00:54.520224038+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello, xxxxx@xxxxx, xxxxx@xxxxx !!","stack_trace":""}
{"@timestamp":"2023-04-23T23:00:54.520538823+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello, xxxxx@xxxxx, xxxxx@xxxxx !!","stack_trace":""}
{"@timestamp":"2023-04-23T23:00:54.521686038+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello, xxxxx@xxxxx !!","stack_trace":"java.lang.RuntimeException: java.lang.RuntimeException: Oops!! xxxxx@xxxxx\n\tat org.littlewings.logback.App.main(App.java:17)\n\tat org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\nCaused by: java.lang.RuntimeException: Oops!! xxxxx@xxxxx\n\tat org.littlewings.logback.App.throwException(App.java:22)\n\tat org.littlewings.logback.App.main(App.java:15)\n\t... 2 common frames omitted\n"}

自䜜の倉換指定子を䜿うのもいいかもですね。

src/main/resources/logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <conversionRule conversionWord="email" converterClass="org.littlewings.logback.EmailReplaceConverter" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
      <providers>
        <timestamp/>
        <version/>
        <!-- <message /> -->
        <loggerName />
        <threadName />
        <logLevel />
        <!-- <stackTrace /> -->
        <context />
        <mdc/>
        <tags />
        <arguments />
        <pattern>
          <pattern>
            {
              "message": "%email(%msg)",
              "stack_trace": "%email(%ex)"
            }
          </pattern>
        </pattern>
      </providers>
    </encoder>
  </appender>

  <root level="info">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

結果。

{"@timestamp":"2023-04-23T23:14:47.362809176+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello World!!","stack_trace":""}
{"@timestamp":"2023-04-23T23:14:47.368164723+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello, [xxxxx], [xxxxx] !!","stack_trace":""}
{"@timestamp":"2023-04-23T23:14:47.36849854+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello, [xxxxx], [xxxxx] !!","stack_trace":""}
{"@timestamp":"2023-04-23T23:14:47.36955851+09:00","@version":"1","logger_name":"org.littlewings.logback.App","thread_name":"org.littlewings.logback.App.main()","level":"INFO","message":"Hello, [xxxxx] !!","stack_trace":"java.lang.RuntimeException: java.lang.RuntimeException: Oops!! [xxxxx]\n\tat org.littlewings.logback.App.main(App.java:17)\n\tat org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n\tat java.base/java.lang.Thread.run(Thread.java:833)\nCaused by: java.lang.RuntimeException: Oops!! [xxxxx]\n\tat org.littlewings.logback.App.throwException(App.java:22)\n\tat org.littlewings.logback.App.main(App.java:15)\n\t... 2 common frames omitted\n"}

MaskingJsonGeneratorDecoratorを䜿ったマスキングず比べるず、察象のフィヌルドを絞れる分だけ効率的ではないのかなず
思うのですが、どうなのでしょう。

ずりあえず、Logstash Logback Encoderを䜿った堎合はこんなずころでしょうか。

たずめ

LogbackおよびLogstash Logback Encoderを䜿っお、文字列眮換を行う方法をいろいろ調べおみたした。

いろいろ方法があるんですね。たた倉換指定子に関する知識の芋盎しになったずいう意味でも、ちゃんず芋おおいおよかったです。