JJUGナイトセミナー「OpenTelemetryとSpring Boot 4.0で進化するJavaアプリの可観測性」
はじめに
基本的にメモそのまま。
APMの世界から見るOpenTelemetryのTraceの世界
Opentelemetryとは?
ログ、メトリックス、トレースを集めてエクスポートする、言語に依存しない標準規格です。
Viewerは「Backend Service」
Telemetryから見たら、Viewerは「バックエンド」という扱い。
Viewerは規格外なので、インターフェースを満たせば何を使っても良い(mackerel、Grafana etc)
開発中でも使うと良さそう
あんまりインフラを触るのは得意ではないけれど、デバッグ用にAOPであれこれしていたところをやらなくても、メトリックスとして取得できるのであれば、それで解決するのはアリなのかもしれない(ので、ちょっと頑張ってみようかな)。
spanをつかってピンポイントに確認できる
デバッグで調査をするところを、あえて、OTLPを使っていくみたいなことをして、ちょっとずつツールに慣れるというのもやってみても良いかもしれない?
バッチに有効そう
バッチに自分でログを入れていたところを簡略化できそう。
テストのないシステムのバージョンアップに非常に協力
何がどこで動いているのか?というのを実装を変えることなく、そとから覗き込むことができるというのはたしかに有効そう
自動計装と手動計装をMIXするときに気をつけること
命名でどちらの計装かわかるようにしておいたほうが良いかも、とのこと。
サンプリングをするときのポイントは?
ログでクリティカルが出ているけど、メトリックスで出ていないみたいなことはありえる。
いずれにしても「?」なことがあったら、ログを元に注視する対象(サーバ、アプリ(Span))を絞ってサンプリングの対象を決める感じかなぁ。
Spring Boot 4.0で実装と監視を分離する
これまでの歴史のおさらいがあるのは嬉しい
DynaTraceは、少しだけみたことあるので、ちょっと懐かしい(2019年頃?)。
形としてまとまり始めたのは2018年くらいみたいなので、アーリー的な時期だったのか……(そりゃ僕には荷が重い)。
アプリケーションが監視環境を知っていなければならないのか?
NOというのが適切な構造なのでは?というのは納得するところ。
サイドカーパターンにして、アプリケーションはしならくて良いっていうのが良いとなぁと思う。
ただ、開発環境で簡易的にやろうと思うと、サイドカーを準備する環境構築することとか考えると躊躇はするんだけれど……
Spring CloudとSpring Bootは違うチーム
という理解であっているかな?
とにかく、そんな感じの関係性らしい。
Spring Boot 4でOTLPの依存をいれるだけでOK?
方向性としてはYESなんだけど、ログについてはlogback-appenderがアルファ版なので、正式にはまだ未対応。
そんなときには、マキさんのブログへGO(https://ik.am/entries/892)
環境構築のパターン
ローカルから本番までの基本構成を提示してくれているのは嬉しい。
大規模の場合のKafkaのパターンも嬉しい。
ポイントは、アプリケーションはOTLPへの依存をpomで設定するだけで完結しているところ。
Grafana
AIをつかって環境構築をされたそうですが、こちらを参考にすればよいとのこと。
Docker OpenTelemetry LGTM | OpenTelemetry documentation
さいごに
いままで、腰が引けてしまう「監視」が、こんなに敷居が低くなっているとは思わなかった。
やってみようと思えるくらい低くなったと思う。
前日に引き続き、懇親会に参加。
EE勉強会にも参加していらっしゃった方と お話させていただいた。
お互いに「現在進行形ではEEを使ってはいないけど、情報収集や動向把握の一環として」というのは同じでした。
あとは、流れで EE/MicroProfile のOTLP周りはどうなんでしょうね、とかとか。
標準仕様を決める合意形成が終わっているのであれば、一気に開発を進めてくれるんじゃないかと思ったり思わなかったり……
JakartaEE&MicroProfile(JakartaCDI特集)
はじめに
久しぶりの勉強会の備忘です。
自分向けのメモを書いていたりはしていたのですが、スライドを見ればわかることしか書いていないし、最近はブログに書くまではしなくなっていました。
今回は、Jakarta EEの勉強会(1回目)ということと、参加者として貢献できるところもあるのかな?と思い直して今に至ります。
CDIの誤解しがちな仕様とその対処TIPS
https://github.com/jjugnsjeecditips202602
WELD-000075で困った話
デフォルトのスコープが違うところは、Springの挙動とCDIの挙動の違いとしては顕著なところかもしれないですね。
EEを触っていたのが随分と前だったこともあって、Managed Beanのスコープ設定の扱いとかすっかり忘れていました&SpringはAppicationScopeがデフォルトというのに始めは戸惑ったことを逆に思い出しました。
記憶としては曖昧ですけど、この辺のスコープ絡みでトランザクションとかで、ハマったというか困ったことがあって、最終的にサービスを Request Scopedにして「ま、これでいいか」としたことがあったような無かったような……
今回の話を踏まえて考え直したら、実はそんなことはしなくても良いみたいなことになるのかもしれないけど、それは未来の宿題にしておこうと思います。
そういえば、どうしてEEは状態を持つ前提なんだったっけ?と思ったら、そうだった JSF*1があったからか……
こちらもむかーし Conversation Scopedで色々と思考実験(?)みたいなのをしたこともあったなぁ。
JSFは嫌いじゃないけど、リソースの活用とか考えたり、Spring MVC(アクション指向)になれると状態を持つというのがなれないというか、気が抜けないというか、そんな感覚に最近はなっているかもしれない。
まぁ、常識の範囲の使い方であれば特に問題もないし、結局は慣れみたいなところだとは思いますけど。
いずれにしても、CDIは状態管理が Springと違うから、気を付けないといけないポイントだな!というのを頭の片隅に置いておくことは大事そう。
Dependentスコープのインスタンスがメモリ解放されない
対策は、CDI.current().destroy() でちゃんと開放しないといけないよ、とのこと。
Quarkus のCDI Liteの実装である ArC だと発生しないとか、AOPアノテーションが付いていなかったら発生しないとか、Weldの実装によるものとのこと。
EE(Full)は、EEサーバ自体にマシンパワーを求めて、そこで中央集権的に処理をするという思想だったりするのかな?
勝手なイメージとしては、アノテーションが付いているフィールドは状態を保持する可能性があるから、能動的に破棄はしないで、EEサーバのメモリ上にキャッシュ的に持っておくみたいな発想があるのかなぁ。
CDI Liteは仕様わからないけど、EE(Full)と違って、マイクロサービス向けで、キャッシュをもって高速化するというよりも、小さく持ってすぐ捨てる、みたいな実装しそうがあるから、とかあったりするのかぁ。
あと、CDIのコンポーネントをコンテナから直接取得するというのは、基盤的なところを実装したらやりたくなるのかな?
積極的に採用することはなさそうだということと、横断的関心毎であればAOPで制御をするかなぁと思っているので、正直なところ具体的なユースケースがわからなかったかも。
あえて思ったのは、エクセルでクラスパスのコンテナ管理の定義をつくって、それを読み込ませることでソースコードを触らなくてもいろいろできますよ系とかかなぁ?
Map<String(識別子), String(クラスパス)>な構造体にして、何でもできますよみたいな*2。
大量のInjectをすることの忌避感自体は理解はするし、実際、Springでもユースケース的につかうサービスは、そうなりがち。
仕方ないのかなぁと正直思っているし、アクション単位でクラスを分けるとか、そんな感じでクラス分割をするとか、そっちの方向で僕はどうにかしようとしている気がするかも。
あとは、状態管理(コンテナ管理)をしないといけないオブジェクトなのか?っていうのは再考したいところかもしれない。
CDIはManeged Bean的な文化(?)があるので、引数で渡せばいいものを、そうしないでRequest Scopedにして Injectして使用するとかしがちかもしれない*3。
そのあたりを見直しするだけでも変わってくるのかもしれない。
とはいえ、JSFを使い始めたら「あぁ、、そうだったそうだった」っていうことに直面はしそう*4。
LangChain4jとは一味違うLangChain4j-CDI
https://github.com/fujitsu/app_blog/tree/master/lc4j-cdi-samples
EEといえば、数村さん。
EE関連の情報の中で、EEサーバの商品説明(?)ではなく、標準化として今何をやっていて、こういうこともできるようになっているよという情報収取をしたい場合は、やっぱり数村さんのセッションという感じ*5。
そして、話を聞くことが優先されてしまってメモを取っていないというのも毎度の話……
資料メインのときは、資料がわかりやすいのでメモを取る必要がなく、デモ中心のときはデモがスムーズすぎてメモを取る暇がない……
AIの変遷
AIといえば、LLMという感じだけど、そうそう元々(?)は、こんな感じだったこんな感じだった、というのを再認識。
LangChain4j
使いやすそう。
RAGを駆使したら、プロダクト専用のプロンプトフォームのバックエンドとか作れるのかもしれない。
バックエンドをクローズドなローカルLLMにしておいて社内QAとか、クローズドな環境用のプロンプトに使うとか、そういうときの実装イメージを感じられて面白かったです。
LangChain4j-CDI
名前は似ているけど、全く別物とのこと。
個人的には、単体仕様のときは それでいいとして、標準仕様になるときにはネームスペースの問題も含めて、製品に偏った表現はやめたほうが良さそう、、と素人ながらに思いました。
AI-CDI(Spring AIに近い命名イメージ)とか、AIもMLとかもあるので、LLM-CDIにするとか。
jakartaにするのか、 io.opentelemetryみたいな感じで もっと標準的にいくのか、org.eclipse.microprofileにしてしまってConfigみたいにいつまでもEEに入らないようになってしまうのか*6。
名前付けは本当に難しい……
さいごに
後で疲れてしまうので最近は不参加なことが多かったのですが*7、今回はEEだし 似たような技術背景の人が多いだろうと思って参加しました。
@yusuke さんと色々なお話をさせていただき考えるヒントを持ち帰れたのはよかったです!*8
僕は文系プログラマですが20年以上かけて積み上げたものがあるのでどうにかなっている(?)ところはありますけど、AIによる実装の加速度的な変化とか、そもそも知っておくべきことの多さとか、本当に今の若い人は大変だなぁと。
とりあえず、僕は、若い人たちの よき踏み石になって、明日から頑張ろうと ちょっとだけ思いました。
flywayのメモ
Maven Goal - Flyway - Product Documentation
Flywayのマイグレーションの管理を考えてみる(Spring Bootでのサンプル付き) - CLOVER🍀
【Java】シンプルなデータベースマイグレーションツール「flyway」を導入する - ほんじゃーねっと
Flyway は複数人での開発に向かないという誤解について - tototoshi の日記
FlywayをJavaプログラムから使ってみる、その1 | GWT Center
SpringBoot × Flyway3パターン(FlywayAutoConfiguration/Java API/Mavenプラグイン) #Java - Qiita
Lombokのメモ
使い方
Lombok Experimental features - abcdefg.....
Lombokの@BuilderがCSVファイル生成に役立った話 - STORES Product Blog
JavaDoc生成の際、Lombokの@Builderを使うと「エラー:シンボルを見つけられません」が発生する #javadoc - Qiita
SpotBugsが可変オブジェクトでないものを可変オブジェクトと判定してしまう場合の対処法 - エキサイト TechBlog.
Lombokを使っているときにJacocoのカバレッジから自動生成分を除外する方法 #Java - Qiita
lombok.config
lombok.extern.findbugs.addSuppressFBWarnings = true lombok.addLombokGeneratedAnnotation = true
OpenAPI(MicroProfile)を拡張してみる
はじめに
でOpenAPI(MicroProfile)の実装をしてみましたが、もう少し楽ができるようにしたいと思いました。
かといって実装そのものを拡張したり大がかりなものをつくるほどでも無い「程よい程度に」拡張をしてみました。
拡張ポイント
プロジェクト全体の定義
OpenApiModelReader(OASModelReaderの実装)に
- Lisence
- ドキュメント
- セキュリティ(トークンの設定)
などのプロジェクトの基本情報を設定します。
オブジェクトのSchemaを追加
OpenApiSchemaに変換をしたいクラスと refで使用するキー値を登録します。
public static final String Gender = prifixPath + "Gender";
キー値のペアとなる、Schema生成関数も登録します.
// openApiのUtilは実装依存となるため実行時に決定するように関数で指定します. private static final Map<String, Supplier<Schema>> schemaMap = Map.ofEntries( entry(Gender, () -> OpenApiSchemaUtil.createEnumSchema(Gender.class)), entry(Genders, () -> OpenApiSchemaUtil.createEnumListSchema(Gender.class)), entry(UploadFile, () -> OpenApiSchemaUtil.createUploadFileSchema()), entry(UploadFiles, () -> OpenApiSchemaUtil.createUploadFileListSchema()));
実際に生成するのは OpenApiModelReader#buildModel内でOpenApiSchema.appendSchema(components);で行います。
登録したキー値は@Schemaのrefとして指定をします。
@Parameters({ @Parameter(name = "gender", description = "性別", schema = @Schema(ref = OpenApiSchema.Gender)), @Parameter(name = "name", description = "ユーザー名", example = "user name") }) public Response getUsersByQuery( @QueryParam("gender") Gender gender, @QueryParam("name") String name) {
なにがうれしいか
スキーマの指定をクラスで指定できるので、書き間違いによる実行時エラーが回避できます。
実装
refの指定としてメソッドを使用できないため、実装としては「疑似 Enumなストラテジー」を作って対応をしました。
/** OpenApiSchemaの変換およびSchemaにrefに指定する定数を管理します. */ public class OpenApiSchema { private static final String prifixPath = "#/components/schemas/"; public static final String Gender = prifixPath + "Gender"; public static final String Genders = prifixPath + "Genders"; public static final String UploadFile = prifixPath + "UploadFile"; public static final String UploadFiles = prifixPath + "UploadFiles"; // openApiのUtilは実装依存となるため実行時に決定するように関数で指定します. private static final Map<String, Supplier<Schema>> schemaMap = Map.ofEntries( entry(Gender, () -> OpenApiSchemaUtil.createEnumSchema(Gender.class)), entry(Genders, () -> OpenApiSchemaUtil.createEnumListSchema(Gender.class)), entry(UploadFile, () -> OpenApiSchemaUtil.createUploadFileSchema()), entry(UploadFiles, () -> OpenApiSchemaUtil.createUploadFileListSchema())); /** * プロパティで指定したクラスをSchemaへ変換してOpenAPIのコンポーネントへ追記します. * * @param components OpenAPIのcomponents */ public static void appendSchema(Components components) { validate(); int startIndex = prifixPath.length(); schemaMap.entrySet().stream() .forEach( entrySet -> { var key = entrySet.getKey().substring(startIndex); components.addSchema(key, entrySet.getValue().get()); }); } /** Publicフィールドとスキーマの設定をするMapの整合性が取れていることを検証します. */ private static void validate() { var fieldList = Stream.of(OpenApiSchema.class.getFields()) .filter(f -> f.getType().isPrimitive() == false) .filter(f -> f.getType().isInstance("")) .map(f -> f.getName()) .collect(Collectors.toSet()); if (schemaMap.entrySet().size() != fieldList.size()) { throw new IllegalArgumentException( "public static field is not match schemaMap. append Schema must match."); } } }
@ExampleObjectにjsonを指定
@ExampleObjectのvalueにjsonを記述をすることができますが、resource配下のjsonファイルを指定することはできません。
本来、externalValueはhttpを使用して外部リソースを参照するものですが、あまり使用することは無いと考え、externalValueにリソースパスを指定して読み込めるようにしました。
externalValue = "openapi/user/get_response_default.json"
src/main/resources/openapi/user/get_response_default.json
内部的には、externalValueのjsonを展開した結果をvalueへ転記して、OpenAPI.yamlとして、そのまま使えるようにしています。
なにがうれしいか
同じレスポンスの型を使ったExampleの記述が完結に記載できます。
またエスケープの無いjsonの記述ができるので見やすいです。
実装
OASFilterの実装であるOpenApiFilterでリクエストボディとレスポンスボディの@ExampleObjectの中身を書き換えます.
/** OpenAPIのOASFilterの実装. */ public class OpenApiFilter implements OASFilter { @Override public RequestBody filterRequestBody(RequestBody requestBody) { OpenApiExampleObjectUtil.convertExternalValueToValue(requestBody.getContent()); return OASFilter.super.filterRequestBody(requestBody); } @Override @SuppressWarnings("checkstyle:AbbreviationAsWordInName") public APIResponse filterAPIResponse(APIResponse apiResponse) { OpenApiExampleObjectUtil.convertExternalValueToValue(apiResponse.getContent()); return OASFilter.super.filterAPIResponse(apiResponse); } }
/** OpenApiExampleObjectUtil. */ public class OpenApiExampleObjectUtil { /** * ExampleObjectのExternalValueで指定したJsonをValueとして展開します. * * <p>Contentを直接上書きします. * * <p>上書きに使用した externalValue は消去します. * * <p>valueに記述がある場合はvalueの記述を優先します. * * @param content openApiの@Content * @throws UncheckedIOException IOExceptionが発生したら処理を中断します. */ public static void convertExternalValueToValue(Content content) { content.getMediaTypes().entrySet().stream() .filter(e -> Objects.nonNull(e)) .forEach( e1 -> { e1.getValue().getExamples().entrySet().stream() .filter( e2 -> Objects.isNull(e2.getValue().getValue()) || e2.getValue().getValue().equals("")) .filter(e2 -> Objects.nonNull(e2.getValue().getExternalValue())) .forEach( example -> { var externalValue = example.getValue().getExternalValue(); ClassLoader loader = OpenApiExampleObjectUtil.class.getClassLoader(); try (var inputStream = loader.getResourceAsStream(externalValue)) { if (Objects.isNull(inputStream)) { throw new FileNotFoundException( "externalValue =[" + externalValue + "] cloud not find resource path."); } String json = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); example.getValue().setValue(json); example.getValue().setExternalValue(""); } catch (IOException ex) { throw new UncheckedIOException( "externalValue =[" + externalValue + "] could not find resource path or could not read resource file.", ex); } }); }); } }
モックとしてjsonを使用する
OpenAPIというよりも、RESTfulAPIを便利にするものです。
開発初期では、インターフェースとしてのOpenAPIの定義設定にあわせて アプリケーションサーバーをモックサーバとして使用したいケースがあります。
メソッドの戻り値の型はResponseではなく、直接レスポンスを返却しています。
(ただし、実行結果のHttpが200が固定になります)
public UserResponse.UserResponseBody getJsonUserById(@PathParam("id") String id) { var response = JsonUtil.readFromResource( "openapi/user/get_response_default.json", UserResponse.UserResponseBody.class); return response.get(); }
なにがうれしいか
すでに作成済みの@ExampleObjectのjsonをそのまま使用することができるので楽ができます。
入力値で返却値を変えたい場合は、複数のjsonを作成して引数を元に返却を切り替えるようにするだけで対応できます。
実装
jsonの読み込みでjacksonを使用するので、pom.xmlに依存を追加します。
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> <type>jar</type> </dependency>
モックでの使用を前提としているので、Jsonの操作時のエラーハンドリングを簡略化して*1実行時例外はあえて塗りつぶしています。
/** JsonUtil. */ public class JsonUtil { private static final Logger logger = Logger.getLogger(JsonUtil.class.getName()); /** * Json文字列をクラスにマッピングします. * * @param <T> マッピングするクラスの型 * @param json json文字列 * @param classType マッピングするクラスの型 * @return マッピングしたインスタンス.例外があった場合は {@code Optional.empty()} */ public static <T> Optional<T> read(String json, Class<T> classType) { ObjectMapper mapper = new ObjectMapper(); try { T object = mapper.readValue(json, classType); return Optional.of(object); } catch (JsonProcessingException ex) { logger.log(Level.SEVERE, "Json could not parse.", ex); return Optional.empty(); } } /** * Jsonリソースをクラスにマッピングします. * * @param <T> マッピングするクラスの型 * @param resourcePath リソースパス * @param classType マッピングするクラスの型 * @return マッピングしたインスタンス.例外があった場合は {@code Optional.empty()} */ public static <T> Optional<T> readFromResource(String resourcePath, Class<T> classType) { ClassLoader loader = Thread.currentThread().getContextClassLoader(); try (var inputStream = loader.getResourceAsStream(resourcePath)) { if (Objects.isNull(inputStream)) { throw new FileNotFoundException( "resourcePath =[" + resourcePath + "] cloud not find resource path."); } String json = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); return read(json, classType); } catch (IOException ex) { logger.log(Level.SEVERE, "resourcePath =[" + resourcePath + "] ioexception.", ex); } return Optional.empty(); } }
Code
experimentation/ee10-02-openapi at openapi-extend · vermeerlab/experimentation · GitHub
さいごに
色々と自動生成的なことをしようとも思ったのですが、標準機能による拡張ポイントで出来ることにあえて限定しておくほうが、EE系の場合には良いかな?と思って このくらいにしましたが、それでも随分と冗長な記述が減るのではないかな?と思っています。
ここではPayaraを使っているため、MicroProfileの実装としては正直貧弱だと思いますが、QuarkusやHelidonといったMicroProfileの実装を使えば(importにQuarkusのライブラリを使うような拡張を使えば)、もっと便利なものがすでに提供されている可能性は大いにあります。
*1:Eitherの代わりにOptionalで簡易的に処置
KarateのfeatureをOpenAPIから作成(ZenWave Karate IDE)
はじめに
OpenAPIの定義をDSL的に使って、Karateのfeatureを自動生成するVSCodeの拡張です。
基本的な使い方は拡張機能のサイトにある動画を見るのが分かりやすいので割愛します。
ここでは自動生成された資産についての補足です。
認可トークンのファイルの出力場所
OpenAPIで認可トークンありにすると、その辺りの資産も自動生成してくれます。
karate-auth.js に認可トークンに関する共通の設定を記述できます。
ただ、このファイルの生成先のディレクトリがプロジェクトのルートでした。
これだとfeatureからのパス指定位置としてふさわしくないため、テストコードのルートとなるディレクトリにファイルを移動させました。
デフォルトはベーシック認証になっているみたいです。
今回は認可トークンをつかったロジックなどは実装していないのでコメントアウトしました。
自動生成で出来なかったところの手直し
通常のRESTfulな仕様の範囲は特にエラーにならなかったです。
ただファイルのアップロードだけは multi-part で指定してくれなかったので修正をしました*1。
And multipart file file = {read:'test-data/FileToUpload1.txt', filename:'FileToUpload1.txt',Content-type:'mulitpart/form-data'}
Code
で作成したEEのアプリをCargoで起動して、karateのテストを動かします。
手順は、下記のリポジトリのREADMEに記載しているので、そちらを参考にしてください。
experimentation/karate-01-openapi at openapi · vermeerlab/experimentation · GitHub
さいごに
OpenAPIをちゃんとつくると、それをDSLとして色々とできるというのは非常に良いですね。
テストコードの構造化しているところとか、よく見るサンプルよりも一歩踏み込んだ感じなので色々と学びも多かったように思います。