ほかの人に役立つものではなさそうな雑記レベルだが、覚え書きとして。
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側のアプリケーションと結び付けるために、もうちょっとそれっぽいサービスを実装する必要がありそうだが、とっかかりはできた。