0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Nablarchでカスタムロガーを実装し、全てのアプリケーションログに透過的な項目添加を行う方法

0
Last updated at Posted at 2026-05-10

はじめに

Nablarchは、TIS社が提供するアプリケーションフレームワークである。

Nablarchには、JsonLogFormatterというログをJSON化する機能が提供されているが、
OpenTelemetry準拠のような、より高度なユースケースでカスタム項目を透過的に添加する例は提供されていない。

この記事では、ログ出力機能のカスタム実装を行い、透過的にログを出力する例を提示する。

このコードサンプルはアプリケーションの動作例を示した最小限の実装サンプルです。
リクエスト量が著しく多いユースケースで本記事の実装をそのまま使用すると、パフォーマンスが低下する可能性があります

実際にアプリケーションに組み込む場合は、パフォーマンス検証およびコードレビューによる精査・検証を行ってください。

前提条件

  • OpenTelemetry準拠とするログの出力設計は、フューチャーアーキテクト様のログ設計ガイドラインを参考とします
  • 事前に、OpenTelemetry準拠のJavaエージェント及びopentelemetry-apiライブラリが設定されているものとします。ただし、ここではそれらの前提条件を満たす方法は説明しません

ライブラリ実装例

参考:import句(クリックして展開)
package com.example.fw;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.instrumentation.api.incubator.log.LoggingContextConstants;
import nablarch.core.log.basic.JsonLogFormatter;
import nablarch.core.log.basic.JsonLogObjectBuilder;
import nablarch.core.log.basic.LogContext;
import nablarch.core.log.basic.ObjectSettings;
import nablarch.core.repository.SystemRepository;
import nablarch.core.util.StringUtil;
public class OtelJsonLogFormatter extends JsonLogFormatter {

    /**
     * Opentelemetry仕様で定義されたSpanContextとResourceAttributesを含むログ出力項目を生成する。<br />
     * 特定ログ項目を設定で無効にすることはできない。<br />
     * SpanContextが無効だった場合、トレースIDとスパンID、トレースフラグはログに出力しない。<br />
     * @param settings LogFormatterの設定
     * @return ログ出力項目
     */
    @Override
    protected List<JsonLogObjectBuilder<LogContext>> createStructuredTargets(ObjectSettings settings) {
        List<JsonLogObjectBuilder<LogContext>> list = super.createStructuredTargets(settings);
        list.add(new SpanContextBuilder());
        list.add(new OtelAttributeBuilder());
        return list;
    }

    /**
     * OpentelemetryのSpanContextを処理するクラス。
     */
    public static class SpanContextBuilder implements JsonLogObjectBuilder<LogContext> {

        /**
         * {@inheritDoc}
         */
        @Override
        public void build(Map<String, Object> structuredObject, LogContext __) {
            SpanContext context = Span.current().getSpanContext();
            // Span.current()はnonNullが保証されているため、NullPointerExceptionにはならない前提。
            if (!context.isValid()) {
                return;
            }

            // ポイント:structuredObjectに必要な項目を添加する
            structuredObject.put(LoggingContextConstants.TRACE_ID, (context.getTraceId()));
            structuredObject.put(LoggingContextConstants.SPAN_ID, (context.getSpanId()));
            structuredObject.put(LoggingContextConstants.TRACE_FLAGS, (context.getTraceFlags()));
        }
    }

    /**
     * Opentelemetryのリソース属性(`otel.resource.attributes`)およびサービス名(`otel.service.name`)を処理するクラス。
     * リソース属性とサービス名が重複した場合、サービス名が優先される。
     */
    public static class OtelAttributeBuilder implements JsonLogObjectBuilder<LogContext> {

        private static final String RESOURCE_ATTRIBUTE_PROP = "otel.resource.attributes";
        private static final String SERVICE_NAME_PROP = "otel.service.name";

        private static final String SERVICE_NAME_KEY = "service.name";
        private static final String FALLBACK_SERVICE_NAME = "unknown_service:java";

        /**
         * {@inheritDoc}
         */
        @Override
        public void build(Map<String, Object> structuredObject, LogContext __) {

            // TODO:システムリポジトリから取得しているため、パフォーマンス効率が悪い。
            
            String resourceAttributeStr = SystemRepository.get(RESOURCE_ATTRIBUTE_PROP);
            // カンマで区切り、その後=でkey-value化したリソース属性の一覧を取得
            Map<String, String> resourceAttributes = resourceAttributeStr == null ? new HashMap<>():
                StringUtil.split(resourceAttributeStr, ",").stream()
                    .filter(s -> s.contains("="))
                    .map(s -> StringUtil.split(s, "="))
                    .filter(s -> s.size() == 2)
                    .limit(128l)// Opentelemetryの一般的上限
                    .collect(Collectors.toMap(k -> k.get(0), k -> k.get(1)));

            // サービス名を設定
            String ServiceName = Optional.<String>ofNullable(SystemRepository.get(SERVICE_NAME_PROP))
                .or(() -> Optional.ofNullable(resourceAttributes.get(SERVICE_NAME_KEY)))
                .orElse(FALLBACK_SERVICE_NAME);
            structuredObject.put(SERVICE_NAME_KEY, ServiceName);
            resourceAttributes.remove(SERVICE_NAME_KEY);

            // サービス名以外のリソース属性を設定
            for (Entry<String, String> resource : resourceAttributes.entrySet()) {
                structuredObject.put(resource.getKey(), resource.getValue());
            }
        }
    }
}

Javaアクション側の実装

参考:import句(クリックして展開)
package com.example;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.example.dto.SampleUserListDto;
import com.example.entity.SampleUser;

import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import io.opentelemetry.instrumentation.annotations.WithSpan;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.context.Context;
import nablarch.common.dao.EntityList;
import nablarch.common.dao.UniversalDao;
import nablarch.core.log.Logger;
import nablarch.core.log.LoggerManager;
import nablarch.fw.web.HttpRequest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import jakarta.ws.rs.client.ClientBuilder;
    @WithSpan
    private int getCount(@SpanAttribute("userCount") int userCount) {
        SpanContext ctx = Span.fromContext(Context.current()).getSpanContext();
        LOGGER.logInfo("これはトレースIDと任意属性を添加するテストです。", Map.of(
            "userCount", userCount
        ));
        return userCount;
    }
env.properties
#下記項目は環境変数による上書きで無視される。ただし、デフォルト値自体は設定する必要がある。
otel.service.name=unknown_service:java
otel.resource.attributes=deployment.environment.name=local
log.properties
# stdout
writer.stdout.className=nablarch.core.log.basic.StandardOutputLogWriter
writer.stdout.formatter.className=com.example.fw.OtelJsonLogFormatter

コンテナ側の実装

compose.yaml
  example-rest-client:
    environment:
      - JAVA_TOOL_OPTIONS=-javaagent:/elastic-otel-javaagent-1.7.0.jar
      - OTEL_SERVICE_NAME=/example-rest-client
      - OTEL_RESOURCE_ATTRIBUTES=deployment.environment.name=staging,service.criticality=critical

結果

SpanContextBuilderによりspan_idtrace_idtrace_flags
OtelAttributeBuilderによりdeployment.environment.nameservice.criticalityservice.nameが自動的に添加される

透過的ではありませんが、userCountのようにプログラマー側で別途カスタム項目を添加することもできます。

com.example.fw.OtelJsonLogFormatter(アプリケーションログ)
{
  "date": "2026-05-10 15:31:06.949",
  "processingSystem": "jaxrs",
  "deployment.environment.name": "staging",
  "trace_id": "03f61ec30b306348d30fb0900ad5f9c5",
  "bootProcess": "",
  "service.name": "/example-rest-client",
  "span_id": "5cf13ce6dd9f5ca0",
  "message": "これはトレースIDと任意属性を添加するテストです。",
  "userId": "guest",
  "executionId": "202605101531060260001",
  "logLevel": "INFO",
  "userCount": 2,
  "trace_flags": "01",
  "requestId": "/find/jsonClient",
  "service.criticality": "critical",
  "runtimeLoggerName": "com.example.SampleAction",
  "loggerName": "ROO"
}
nablarch.fw.jaxrs.JaxRsAccessJsonLogFormatter(HTTPアクセスログ)
{
  "date": "2026-05-10 15:31:06.985",
  "processingSystem": "jaxrs",
  "deployment.environment.name": "staging",
  "trace_id": "03f61ec30b306348d30fb0900ad5f9c5",
  "bootProcess": "",
  "service.name": "/example-rest-client",
  "span_id": "f2ef0f9dc3371756",
  "executionTime": 953,
  "requestId": "/find/jsonClient",
  "startTime": "2026-05-10 15:31:06.032",
  "label": "HTTP ACCESS END",
  "sessionId": "",
  "endTime": "2026-05-10 15:31:06.985",
  "userId": "guest",
  "maxMemory": 8392802304,
  "freeMemory": 8275474840,
  "url": "http://192.168.1.3:8080/find/jsonClient",
  "statusCode": 200,
  "executionId": "202605101531060260001",
  "logLevel": "INFO",
  "trace_flags": "01",
  "service.criticality": "critical",
  "runtimeLoggerName": "HTTP_ACCESS",
  "loggerName": "ACC"
}

参考文献

Nablarch公式ドキュメント

フューチャーアーキテクト:ログ設計ガイドライン

OpenTelemetry:Record Telemetry with API

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?