ほかの人に役立つものではなさそうな雑記レベルだが、覚え書きとして。
zero-code計装でSpring Bootを残していたのと、デモ用に作ったアプリケーションがRoRのシングルサービスで分散トレーシングの面白みがあまりないので、何かくっつけてみるかーと練習している。
連携サービスを作る前にそもそもSpring Boot何もわからん状態なので、まずはチュートリアルから進めていって、rest-serviceで最低限のことはできそうだなという雰囲気までつかんだ。
ネット記事拾い読みよりは公式チュートリアルで進めたい勢。
チュートリアルに基づくRESTfulなアプリケーション作成
Getting Started | Building a RESTful Web Serviceに書かれていることそのまま。
このアプリケーションでは、/greeting?name=値のGETリクエストをされたら、{ id: シーケンシャルな番号, content: "Hello, <name属性値>" }のJSONを返したい。nameパラメータが省略されたらWorldになる。
Spring InitializrでArtifactに「restservice」のようにアプリケーション名を付ける(NameやPackage nameも追従する)。さらにDependenciesの「ADD DEPENDENCIES...」をクリックし、「Spring Web」を選択する。「GENERATE」をクリックするとzipがダウンロードされる。
Webは別にMVCになっているわけではないので、モデルとコントローラは自分で書く必要がある。ビューはJackson2ライブラリがJSON化をしてくれるため考えなくていい。
モデルを書く(src/main/java/com/example/restservice/Greeting.java)。
package com.example.restservice; public record Greeting(long id, String content) { }
VS Codeでファイルを作るとだいたいできる。モデルはclassでなくrecordにする。ここではモデル内の処理は何もないので、引数にモデルの属性相当を書くだけ。
コントローラを書く(src/main/java/com/example/restservice/GreetingController.java)。
package com.example.restservice; import java.util.concurrent.atomic.AtomicLong; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class GreetingController { private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @GetMapping("/greeting") public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { return new Greeting(counter.incrementAndGet(), String.format(template, name)); } }
VS Codeでimportまわりは考えずにだいたい入る。
@RestControllerアノテーションをクラスに付ける。これでコントローラであることを表す。ビューじゃなくてドメインオブジェクトを返すことを宣言している。@Controlerと@ResponseBodyの2つを設定せずに済むショートカットらしい。
private finalでテンプレート文字列と、インクリメンタルカウンタ用オブジェクトを定義。
リクエスト処理のgreetingメソッドで、リクエストパラメータの引数受け取りと、Greetingモデルの返却を記述する。
@GetMappingアノテーションでそのメソッドについて、リクエストパスやHTTPメソッドとの対応処理をする(POSTなら@PostMappingになるし、@RequestMapping(method=GET)のように細かく設定もできる)。
パラメータのほうは、@RequestParamアノテーションでパラメータと引数変数の対応付けや初期値指定をしている。
返却処理はGreetingオブジェクトを作って返すだけ。
アプリケーションコード(RestServiceApplication.java)は試す段階程度ではInitializrが作ったものから変更する必要がない。
package com.example.restservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RestserviceApplication { public static void main(String[] args) { SpringApplication.run(RestserviceApplication.class, args); } }
起動。
./gradlew bootRun
curlしてみる。
$ curl http://localhost:8080/greeting
{"id":1,"content":"Hello, World!"}
$ curl http://localhost:8080/greeting?name=kmuto
{"id":2,"content":"Hello, kmuto!"}
APIサーバーっぽいのができた。
zero-code計装
トレースを試してみる。
コントローラにエラーも仕込んでおこう。name=errorだったらランタイムエラーを起こすようにした。
@GetMapping("/greeting") public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { if (name.equals("error")) { throw new RuntimeException("An error occurred"); } return new Greeting(counter.incrementAndGet(), String.format(template, name)); }
jarを作る。
./gradlew build
ひとまずJava Agentで見てみよう。opentelemetry-javaagent.jarを持ってきた。デバッグ表示にしたOpenTelemetry Collectorも動かしている(まぁこのくらいならCollector使わずとも普通にloggingに直接出せばいいんじゃないか説ある)。
java -javaagent:./opentelemetry-javaagent.jar -DOtel.service.name=spring-zerocode -jar build/libs/restservice-0.0.1-SNAPSHOT.jar
curlで適当にアクセス。
2025-04-13T14:27:11.591+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
2025-04-13T14:27:11.591+0900 info ResourceSpans #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0
Resource attributes:
-> host.arch: Str(amd64)
-> host.name: Str(myhost)
-> os.description: Str(Linux 6.1.0-30-amd64)
-> os.type: Str(linux)
-> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-DOtel.service.name=spring-zerocode","-jar","build/libs/restservice-0.0.1-SNAPSHOT.jar"])
-> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java)
-> process.pid: Int(847474)
-> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.14+7-Debian-1deb12u1)
-> process.runtime.name: Str(OpenJDK Runtime Environment)
-> process.runtime.version: Str(17.0.14+7-Debian-1deb12u1)
-> service.instance.id: Str(7fbf7d16-60ae-4f6b-a1cf-046dbcc0d586)
-> service.name: Str(spring-zerocode)
-> service.version: Str(0.0.1-SNAPSHOT)
-> telemetry.distro.name: Str(opentelemetry-java-instrumentation)
-> telemetry.distro.version: Str(2.12.0)
-> telemetry.sdk.language: Str(java)
-> telemetry.sdk.name: Str(opentelemetry)
-> telemetry.sdk.version: Str(1.46.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope io.opentelemetry.tomcat-10.0 2.12.0-alpha
Span #0
Trace ID : f451f47b0784ecda700c5883a236b828
Parent ID :
ID : dd6f9a9a77f5e13c
Name : GET /greeting
Kind : Server
Start time : 2025-04-13 05:27:11.269152171 +0000 UTC
End time : 2025-04-13 05:27:11.270719808 +0000 UTC
Status code : Unset
Status message :
Attributes:
-> network.peer.address: Str(127.0.0.1)
-> server.address: Str(localhost)
-> client.address: Str(127.0.0.1)
-> url.path: Str(/greeting)
-> server.port: Int(8080)
-> http.request.method: Str(GET)
-> thread.id: Int(44)
-> http.response.status_code: Int(200)
-> http.route: Str(/greeting)
-> user_agent.original: Str(curl/7.88.1)
-> network.peer.port: Int(38282)
-> network.protocol.version: Str(1.1)
-> url.scheme: Str(http)
-> thread.name: Str(http-nio-8080-exec-6)
{"kind": "exporter", "data_type": "traces", "name": "debug"}
2025-04-13T14:27:21.594+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
2025-04-13T14:27:21.594+0900 info ResourceSpans #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0
Resource attributes:
-> host.arch: Str(amd64)
-> host.name: Str(myhost)
-> os.description: Str(Linux 6.1.0-30-amd64)
-> os.type: Str(linux)
-> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-DOtel.service.name=spring-zerocode","-jar","build/libs/restservice-0.0.1-SNAPSHOT.jar"])
-> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java)
-> process.pid: Int(847474)
-> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.14+7-Debian-1deb12u1)
-> process.runtime.name: Str(OpenJDK Runtime Environment)
-> process.runtime.version: Str(17.0.14+7-Debian-1deb12u1)
-> service.instance.id: Str(7fbf7d16-60ae-4f6b-a1cf-046dbcc0d586)
-> service.name: Str(spring-zerocode)
-> service.version: Str(0.0.1-SNAPSHOT)
-> telemetry.distro.name: Str(opentelemetry-java-instrumentation)
-> telemetry.distro.version: Str(2.12.0)
-> telemetry.sdk.language: Str(java)
-> telemetry.sdk.name: Str(opentelemetry)
-> telemetry.sdk.version: Str(1.46.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope io.opentelemetry.tomcat-10.0 2.12.0-alpha
Span #0
Trace ID : 0709c2241c0588cf8ae5b95210231aa0
Parent ID :
ID : a921e14da2f19dfe
Name : GET /greeting
Kind : Server
Start time : 2025-04-13 05:27:16.803467936 +0000 UTC
End time : 2025-04-13 05:27:16.804822312 +0000 UTC
Status code : Unset
Status message :
Attributes:
-> network.peer.address: Str(127.0.0.1)
-> server.address: Str(localhost)
-> client.address: Str(127.0.0.1)
-> url.path: Str(/greeting)
-> url.query: Str(name=kmuto)
-> server.port: Int(8080)
-> http.request.method: Str(GET)
-> thread.id: Int(45)
-> http.response.status_code: Int(200)
-> http.route: Str(/greeting)
-> user_agent.original: Str(curl/7.88.1)
-> network.peer.port: Int(38392)
-> network.protocol.version: Str(1.1)
-> url.scheme: Str(http)
-> thread.name: Str(http-nio-8080-exec-7)
{"kind": "exporter", "data_type": "traces", "name": "debug"}
2025-04-13T14:27:24.925+0900 info Logs {"kind": "exporter", "data_type": "logs", "name": "debug", "resource logs": 1, "log records": 1}
2025-04-13T14:27:24.925+0900 info ResourceLog #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0
Resource attributes:
-> host.arch: Str(amd64)
-> host.name: Str(myhost)
-> os.description: Str(Linux 6.1.0-30-amd64)
-> os.type: Str(linux)
-> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-DOtel.service.name=spring-zerocode","-jar","build/libs/restservice-0.0.1-SNAPSHOT.jar"])
-> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java)
-> process.pid: Int(847474)
-> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.14+7-Debian-1deb12u1)
-> process.runtime.name: Str(OpenJDK Runtime Environment)
-> process.runtime.version: Str(17.0.14+7-Debian-1deb12u1)
-> service.instance.id: Str(7fbf7d16-60ae-4f6b-a1cf-046dbcc0d586)
-> service.name: Str(spring-zerocode)
-> service.version: Str(0.0.1-SNAPSHOT)
-> telemetry.distro.name: Str(opentelemetry-java-instrumentation)
-> telemetry.distro.version: Str(2.12.0)
-> telemetry.sdk.language: Str(java)
-> telemetry.sdk.name: Str(opentelemetry)
-> telemetry.sdk.version: Str(1.46.0)
ScopeLogs #0
ScopeLogs SchemaURL:
InstrumentationScope org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet]
LogRecord #0
ObservedTimestamp: 2025-04-13 05:27:24.405389485 +0000 UTC
Timestamp: 2025-04-13 05:27:24.405 +0000 UTC
SeverityText: SEVERE
SeverityNumber: Error(17)
Body: Str(Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: An error occurred] with root cause)
Attributes:
-> exception.message: Str(An error occurred)
-> exception.stacktrace: Str(java.lang.RuntimeException: An error occurred
at com.example.restservice.GreetingController.greeting(GreetingController.java:18)
...
at java.base/java.lang.Thread.run(Thread.java:840)
)
-> exception.type: Str(java.lang.RuntimeException)
Trace ID: e708f6f8d9877efc26ac854abc78a816
Span ID: 889d1cb3bd67b211
Flags: 1
{"kind": "exporter", "data_type": "logs", "name": "debug"}
2025-04-13T14:27:26.595+0900 info Traces {"kind": "exporter", "data_type": "traces", "name": "debug", "resource spans": 1, "spans": 1}
2025-04-13T14:27:26.595+0900 info ResourceSpans #0
Resource SchemaURL: https://opentelemetry.io/schemas/1.24.0
Resource attributes:
-> host.arch: Str(amd64)
-> host.name: Str(myhost)
-> os.description: Str(Linux 6.1.0-30-amd64)
-> os.type: Str(linux)
-> process.command_args: Slice(["/usr/lib/jvm/java-17-openjdk-amd64/bin/java","-javaagent:./opentelemetry-javaagent.jar","-DOtel.service.name=spring-zerocode","-jar","build/libs/restservice-0.0.1-SNAPSHOT.jar"])
-> process.executable.path: Str(/usr/lib/jvm/java-17-openjdk-amd64/bin/java)
-> process.pid: Int(847474)
-> process.runtime.description: Str(Debian OpenJDK 64-Bit Server VM 17.0.14+7-Debian-1deb12u1)
-> process.runtime.name: Str(OpenJDK Runtime Environment)
-> process.runtime.version: Str(17.0.14+7-Debian-1deb12u1)
-> service.instance.id: Str(7fbf7d16-60ae-4f6b-a1cf-046dbcc0d586)
-> service.name: Str(spring-zerocode)
-> service.version: Str(0.0.1-SNAPSHOT)
-> telemetry.distro.name: Str(opentelemetry-java-instrumentation)
-> telemetry.distro.version: Str(2.12.0)
-> telemetry.sdk.language: Str(java)
-> telemetry.sdk.name: Str(opentelemetry)
-> telemetry.sdk.version: Str(1.46.0)
ScopeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope io.opentelemetry.tomcat-10.0 2.12.0-alpha
Span #0
Trace ID : e708f6f8d9877efc26ac854abc78a816
Parent ID :
ID : 889d1cb3bd67b211
Name : GET /greeting
Kind : Server
Start time : 2025-04-13 05:27:24.404152261 +0000 UTC
End time : 2025-04-13 05:27:24.407718876 +0000 UTC
Status code : Error
Status message :
Attributes:
-> network.peer.address: Str(127.0.0.1)
-> server.address: Str(localhost)
-> client.address: Str(127.0.0.1)
-> url.path: Str(/greeting)
-> error.type: Str(500)
-> url.query: Str(name=error)
-> server.port: Int(8080)
-> http.request.method: Str(GET)
-> thread.id: Int(46)
-> http.response.status_code: Int(500)
-> http.route: Str(/greeting)
-> user_agent.original: Str(curl/7.88.1)
-> network.peer.port: Int(40870)
-> network.protocol.version: Str(1.1)
-> url.scheme: Str(http)
-> thread.name: Str(http-nio-8080-exec-8)
Events:
SpanEvent #0
-> Name: exception
-> Timestamp: 2025-04-13 05:27:24.407653797 +0000 UTC
-> DroppedAttributesCount: 0
-> Attributes::
-> exception.message: Str(An error occurred)
-> exception.type: Str(java.lang.RuntimeException)
-> exception.stacktrace: Str(java.lang.RuntimeException: An error occurred
at com.example.restservice.GreetingController.greeting(GreetingController.java:18)
...
at java.base/java.lang.Thread.run(Thread.java:840)
)
{"kind": "exporter", "data_type": "traces", "name": "debug"}
手動計装
スパンを作ってみる。
OTel SDKを依存に組み込む(build.gradle)。
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.opentelemetry:opentelemetry-api:1.46.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
バージョンは使っているJava Agentのものと合わせる。
コントローラに仕込んでみる。
package com.example.restservice; import java.util.concurrent.atomic.AtomicLong; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; @RestController public class GreetingController { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("com.example.restservice"); private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @GetMapping("/greeting") public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { Span span = tracer.spanBuilder("greeting").startSpan(); span.setAttribute("name", name); span.setAttribute("counter", counter.get()); try { if (name.equals("error")) { throw new RuntimeException("An error occurred"); } return new Greeting(counter.incrementAndGet(), String.format(template, name)); } finally { span.end(); } } }
importはCopilotのクイックフィクスに任せた。getTracerの引数はmust not be nullだが、ユニークでありさえすればよさそう。
メソッド内では取得したtracerに基づいてスパンを作る。
copeSpans #0
ScopeSpans SchemaURL:
InstrumentationScope io.opentelemetry.tomcat-10.0 2.12.0-alpha
Span #0
Trace ID : a2ea4cea68f42280320ce554034a6e69
Parent ID :
ID : 2ff0459bc123f734
Name : GET /greeting
Kind : Server
Start time : 2025-04-13 07:23:54.219432765 +0000 UTC
End time : 2025-04-13 07:23:54.335554619 +0000 UTC
Status code : Unset
Status message :
Attributes:
-> network.peer.address: Str(127.0.0.1)
-> server.address: Str(localhost)
-> client.address: Str(127.0.0.1)
-> url.path: Str(/greeting)
-> url.query: Str(name=kmuto)
-> server.port: Int(8080)
-> http.request.method: Str(GET)
-> thread.id: Int(39)
-> http.response.status_code: Int(200)
-> http.route: Str(/greeting)
-> user_agent.original: Str(curl/7.88.1)
-> network.peer.port: Int(54680)
-> network.protocol.version: Str(1.1)
-> url.scheme: Str(http)
-> thread.name: Str(http-nio-8080-exec-1)
ScopeSpans #1
ScopeSpans SchemaURL:
InstrumentationScope com.example.restservice
Span #0
Trace ID : a2ea4cea68f42280320ce554034a6e69
Parent ID : 2ff0459bc123f734
ID : d8cd6a2ce10f3673
Name : greeting
Kind : Internal
Start time : 2025-04-13 07:23:54.29686414 +0000 UTC
End time : 2025-04-13 07:23:54.296936405 +0000 UTC
Status code : Unset
Status message :
Attributes:
-> counter: Int(0)
-> thread.id: Int(39)
-> name: Str(kmuto)
-> thread.name: Str(http-nio-8080-exec-1)
{"kind": "exporter", "data_type": "traces", "name": "debug"}
テスト用とはいえ、だいぶ雑な単位でスパン化してしまった。span.end()を省略してみたところ、虚空に消えてしまうっぽい。try-catch-finallyが微妙な気がしていたところ、try-with-resourcesを使うと読みやすくなるらしい。
package com.example.restservice; import java.util.concurrent.atomic.AtomicLong; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; @RestController public class GreetingController { private static final Tracer tracer = GlobalOpenTelemetry.getTracer("com.example.restservice"); private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @GetMapping("/greeting") public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { long counterVal = counter.incrementAndGet(); Span span = tracer.spanBuilder("greeting") .setAttribute("name", name) .setAttribute("counter", counterVal) .startSpan(); try (Scope scope = span.makeCurrent()) { return greetingImpl(name, counterVal); } catch (Exception e) { span.recordException(e); span.setStatus(io.opentelemetry.api.trace.StatusCode.ERROR); throw e; // rethrow the exception after recording it } finally { span.end(); } } private Greeting greetingImpl(String name, long counterVal) { if (name.equals("error")) { throw new RuntimeException("An error occurred"); } return new Greeting(counterVal, String.format(template, name)); } }
雑な切り出しだが、なるほど。makeCurrentで現在のスレッドのコンテキストにスパンが紐づくので、ここから子のHTTP呼び出し、DBトレースなどもこの中に入っていくことになる。
ラッパーのユーティリティメソッドを作ってしまう手もある。
package com.example.restservice; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Scope; @RestController public class GreetingController { private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @GetMapping("/greeting") public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { return tracedSpan("greeting", span -> { long counterVal = counter.incrementAndGet(); span.setAttribute("name", name); span.setAttribute("counter", counterVal); if (name.equals("error")) { throw new RuntimeException("An error occurred"); } return new Greeting(counterVal, String.format(template, name)); }); } public static <T> T tracedSpan(String name, Function<Span, T> logic) { Tracer tracer = GlobalOpenTelemetry.getTracer("com.example.restservice"); Span span = tracer.spanBuilder(name).startSpan(); try (Scope scope = span.makeCurrent()) { return logic.apply(span); } catch (Exception e) { span.recordException(e); span.setStatus(io.opentelemetry.api.trace.StatusCode.ERROR); throw e; // rethrow the exception after recording it } finally { span.end(); } } }
これはだいぶわかりやすくなった気がする。

transparentによる引き継ぎ
Rails側ではOTel SDK側でtransparentヘッダーを付加するよう設定されており、Java Agentはリクエストのtransparentを自動解釈して自動で親スパン扱いにして引き継いでくれる。
……というところで今日は時間切れ。
Rails側のアプリケーションと結び付けるために、もうちょっとそれっぽいサービスを実装する必要がありそうだが、とっかかりはできた。