はじめに
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;
}
#下記項目は環境変数による上書きで無視される。ただし、デフォルト値自体は設定する必要がある。
otel.service.name=unknown_service:java
otel.resource.attributes=deployment.environment.name=local
# stdout
writer.stdout.className=nablarch.core.log.basic.StandardOutputLogWriter
writer.stdout.formatter.className=com.example.fw.OtelJsonLogFormatter
コンテナ側の実装
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_idとtrace_id、trace_flagsが
OtelAttributeBuilderによりdeployment.environment.name、service.criticality、service.nameが自動的に添加される
透過的ではありませんが、userCountのようにプログラマー側で別途カスタム項目を添加することもできます。
{
"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"
}
{
"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