From af0facc09cb2babef138fd84c3241bf6449d0a04 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 8 Apr 2026 14:05:46 -0300 Subject: [PATCH 01/11] prepare for next development cycle --- jooby/pom.xml | 2 +- modules/jooby-apt/pom.xml | 2 +- modules/jooby-avaje-inject/pom.xml | 2 +- modules/jooby-avaje-jsonb/pom.xml | 2 +- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v1/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-bom/pom.xml | 45 ++++++++++++++++++++--- modules/jooby-caffeine/pom.xml | 2 +- modules/jooby-camel/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-commons-email/pom.xml | 2 +- modules/jooby-conscrypt/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-distribution/pom.xml | 2 +- modules/jooby-ebean/pom.xml | 2 +- modules/jooby-flyway/pom.xml | 2 +- modules/jooby-freemarker/pom.xml | 2 +- modules/jooby-gradle-setup/pom.xml | 2 +- modules/jooby-graphiql/pom.xml | 2 +- modules/jooby-graphql/pom.xml | 2 +- modules/jooby-grpc/pom.xml | 2 +- modules/jooby-gson/pom.xml | 2 +- modules/jooby-guice/pom.xml | 2 +- modules/jooby-handlebars/pom.xml | 2 +- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-hibernate/pom.xml | 2 +- modules/jooby-hikari/pom.xml | 2 +- modules/jooby-jackson/pom.xml | 2 +- modules/jooby-jackson3/pom.xml | 2 +- modules/jooby-jasypt/pom.xml | 2 +- modules/jooby-javadoc/pom.xml | 2 +- modules/jooby-jdbi/pom.xml | 2 +- modules/jooby-jetty/pom.xml | 2 +- modules/jooby-jsonrpc-avaje-jsonb/pom.xml | 2 +- modules/jooby-jsonrpc-jackson2/pom.xml | 2 +- modules/jooby-jsonrpc-jackson3/pom.xml | 2 +- modules/jooby-jsonrpc/pom.xml | 2 +- modules/jooby-jstachio/pom.xml | 2 +- modules/jooby-jte/pom.xml | 2 +- modules/jooby-jwt/pom.xml | 2 +- modules/jooby-kafka/pom.xml | 2 +- modules/jooby-kotlin/pom.xml | 2 +- modules/jooby-langchain4j/pom.xml | 2 +- modules/jooby-log4j/pom.xml | 2 +- modules/jooby-logback/pom.xml | 2 +- modules/jooby-maven-plugin/pom.xml | 2 +- modules/jooby-mcp-jackson2/pom.xml | 2 +- modules/jooby-mcp-jackson3/pom.xml | 2 +- modules/jooby-mcp/pom.xml | 2 +- modules/jooby-metrics/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-netty/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-pac4j/pom.xml | 2 +- modules/jooby-pebble/pom.xml | 2 +- modules/jooby-quartz/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-redis/pom.xml | 2 +- modules/jooby-redoc/pom.xml | 2 +- modules/jooby-rocker/pom.xml | 2 +- modules/jooby-run/pom.xml | 2 +- modules/jooby-rxjava3/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- modules/jooby-swagger-ui/pom.xml | 2 +- modules/jooby-test/pom.xml | 2 +- modules/jooby-thymeleaf/pom.xml | 2 +- modules/jooby-trpc-avaje-jsonb/pom.xml | 2 +- modules/jooby-trpc-generator/pom.xml | 2 +- modules/jooby-trpc-jackson2/pom.xml | 2 +- modules/jooby-trpc-jackson3/pom.xml | 2 +- modules/jooby-trpc/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- modules/jooby-vertx-mysql-client/pom.xml | 2 +- modules/jooby-vertx-pg-client/pom.xml | 2 +- modules/jooby-vertx-sql-client/pom.xml | 2 +- modules/jooby-vertx/pom.xml | 2 +- modules/jooby-whoops/pom.xml | 2 +- modules/jooby-yasson/pom.xml | 2 +- modules/pom.xml | 2 +- pom.xml | 4 +- tests/pom.xml | 2 +- 82 files changed, 122 insertions(+), 87 deletions(-) diff --git a/jooby/pom.xml b/jooby/pom.xml index de1bba9eb4..021b84b2bb 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.3.0 + 4.3.1-SNAPSHOT jooby jooby diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index d97b8599e9..f7f10c34ca 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-apt jooby-apt diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml index 5e59a9acc9..7a7e208488 100644 --- a/modules/jooby-avaje-inject/pom.xml +++ b/modules/jooby-avaje-inject/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-avaje-inject jooby-avaje-inject diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index dd6fa118fc..ea77a28996 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-avaje-jsonb jooby-avaje-jsonb diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index 056f71a3c8..6a391e84a6 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-avaje-validator jooby-avaje-validator diff --git a/modules/jooby-awssdk-v1/pom.xml b/modules/jooby-awssdk-v1/pom.xml index f03f433f5e..df4a7d40fe 100644 --- a/modules/jooby-awssdk-v1/pom.xml +++ b/modules/jooby-awssdk-v1/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-awssdk-v1 jooby-awssdk-v1 diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 9429768e6c..632ca647eb 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-awssdk-v2 jooby-awssdk-v2 diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index 2bf4ccce71..d4e797270a 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -7,14 +7,14 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT io.jooby jooby-bom jooby-bom pom - 4.3.0 + 4.3.1-SNAPSHOT Jooby (Bill of Materials) https://jooby.io @@ -167,17 +167,32 @@ io.jooby - jooby-javadoc + jooby-jdbi ${project.version} io.jooby - jooby-jdbi + jooby-jetty ${project.version} io.jooby - jooby-jetty + jooby-jsonrpc + ${project.version} + + + io.jooby + jooby-jsonrpc-avaje-jsonb + ${project.version} + + + io.jooby + jooby-jsonrpc-jackson2 + ${project.version} + + + io.jooby + jooby-jsonrpc-jackson3 ${project.version} @@ -330,6 +345,26 @@ jooby-trpc ${project.version} + + io.jooby + jooby-trpc-avaje-jsonb + ${project.version} + + + io.jooby + jooby-trpc-generator + ${project.version} + + + io.jooby + jooby-trpc-jackson2 + ${project.version} + + + io.jooby + jooby-trpc-jackson3 + ${project.version} + io.jooby jooby-undertow diff --git a/modules/jooby-caffeine/pom.xml b/modules/jooby-caffeine/pom.xml index f7ed68d5ac..cf04bda03e 100644 --- a/modules/jooby-caffeine/pom.xml +++ b/modules/jooby-caffeine/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-caffeine jooby-caffeine diff --git a/modules/jooby-camel/pom.xml b/modules/jooby-camel/pom.xml index e7eb3b656f..65394c5c9c 100644 --- a/modules/jooby-camel/pom.xml +++ b/modules/jooby-camel/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-camel jooby-camel diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 14447122b6..553dd008ea 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-cli jooby-cli diff --git a/modules/jooby-commons-email/pom.xml b/modules/jooby-commons-email/pom.xml index d95b472561..fd67d2da92 100644 --- a/modules/jooby-commons-email/pom.xml +++ b/modules/jooby-commons-email/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-commons-email jooby-commons-email diff --git a/modules/jooby-conscrypt/pom.xml b/modules/jooby-conscrypt/pom.xml index c2d8352878..870dd164e1 100644 --- a/modules/jooby-conscrypt/pom.xml +++ b/modules/jooby-conscrypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-conscrypt jooby-conscrypt diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index 79bee0e642..3e4e531635 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-db-scheduler jooby-db-scheduler diff --git a/modules/jooby-distribution/pom.xml b/modules/jooby-distribution/pom.xml index 9f23b9a6ce..90b371f136 100644 --- a/modules/jooby-distribution/pom.xml +++ b/modules/jooby-distribution/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-distribution jooby-distribution diff --git a/modules/jooby-ebean/pom.xml b/modules/jooby-ebean/pom.xml index eb8e2408c8..2c3ed0dadc 100644 --- a/modules/jooby-ebean/pom.xml +++ b/modules/jooby-ebean/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-ebean jooby-ebean diff --git a/modules/jooby-flyway/pom.xml b/modules/jooby-flyway/pom.xml index e4468140c4..0555d60a78 100644 --- a/modules/jooby-flyway/pom.xml +++ b/modules/jooby-flyway/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-flyway jooby-flyway diff --git a/modules/jooby-freemarker/pom.xml b/modules/jooby-freemarker/pom.xml index 7702269521..ed5016c5ce 100644 --- a/modules/jooby-freemarker/pom.xml +++ b/modules/jooby-freemarker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-freemarker jooby-freemarker diff --git a/modules/jooby-gradle-setup/pom.xml b/modules/jooby-gradle-setup/pom.xml index a24be40137..35f8752060 100644 --- a/modules/jooby-gradle-setup/pom.xml +++ b/modules/jooby-gradle-setup/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-gradle-setup jooby-gradle-setup diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index c7cbedda46..3122a7ab83 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-graphiql jooby-graphiql diff --git a/modules/jooby-graphql/pom.xml b/modules/jooby-graphql/pom.xml index 1652e41131..6d07951e05 100644 --- a/modules/jooby-graphql/pom.xml +++ b/modules/jooby-graphql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-graphql jooby-graphql diff --git a/modules/jooby-grpc/pom.xml b/modules/jooby-grpc/pom.xml index 8c38b0d926..08906bdabb 100644 --- a/modules/jooby-grpc/pom.xml +++ b/modules/jooby-grpc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-grpc jooby-grpc diff --git a/modules/jooby-gson/pom.xml b/modules/jooby-gson/pom.xml index f00409eda8..674f735c2a 100644 --- a/modules/jooby-gson/pom.xml +++ b/modules/jooby-gson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-gson jooby-gson diff --git a/modules/jooby-guice/pom.xml b/modules/jooby-guice/pom.xml index d6f5b11fe5..ce8fadf0f7 100644 --- a/modules/jooby-guice/pom.xml +++ b/modules/jooby-guice/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-guice jooby-guice diff --git a/modules/jooby-handlebars/pom.xml b/modules/jooby-handlebars/pom.xml index 0c3c442b35..cdb6fe78cd 100644 --- a/modules/jooby-handlebars/pom.xml +++ b/modules/jooby-handlebars/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-handlebars jooby-handlebars diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index 4e34f44cc9..e8cb48191e 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-hibernate-validator jooby-hibernate-validator diff --git a/modules/jooby-hibernate/pom.xml b/modules/jooby-hibernate/pom.xml index 4f82dfc667..161100d3bb 100644 --- a/modules/jooby-hibernate/pom.xml +++ b/modules/jooby-hibernate/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-hibernate jooby-hibernate diff --git a/modules/jooby-hikari/pom.xml b/modules/jooby-hikari/pom.xml index 13516e80fb..51efef5bad 100644 --- a/modules/jooby-hikari/pom.xml +++ b/modules/jooby-hikari/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-hikari jooby-hikari diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index b173312984..959b100a9b 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jackson jooby-jackson diff --git a/modules/jooby-jackson3/pom.xml b/modules/jooby-jackson3/pom.xml index 266db1b7a1..908db0a760 100644 --- a/modules/jooby-jackson3/pom.xml +++ b/modules/jooby-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jackson3 jooby-jackson3 diff --git a/modules/jooby-jasypt/pom.xml b/modules/jooby-jasypt/pom.xml index ea7174963f..981aa518a9 100644 --- a/modules/jooby-jasypt/pom.xml +++ b/modules/jooby-jasypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jasypt jooby-jasypt diff --git a/modules/jooby-javadoc/pom.xml b/modules/jooby-javadoc/pom.xml index 6063eaab96..eec2533dfa 100644 --- a/modules/jooby-javadoc/pom.xml +++ b/modules/jooby-javadoc/pom.xml @@ -8,7 +8,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-javadoc jooby-javadoc diff --git a/modules/jooby-jdbi/pom.xml b/modules/jooby-jdbi/pom.xml index 8c2d7c2f83..b28c52bedc 100644 --- a/modules/jooby-jdbi/pom.xml +++ b/modules/jooby-jdbi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jdbi jooby-jdbi diff --git a/modules/jooby-jetty/pom.xml b/modules/jooby-jetty/pom.xml index 3a1f0a43ef..baeda2a4bc 100644 --- a/modules/jooby-jetty/pom.xml +++ b/modules/jooby-jetty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jetty jooby-jetty diff --git a/modules/jooby-jsonrpc-avaje-jsonb/pom.xml b/modules/jooby-jsonrpc-avaje-jsonb/pom.xml index 2d4658e568..2f21820190 100644 --- a/modules/jooby-jsonrpc-avaje-jsonb/pom.xml +++ b/modules/jooby-jsonrpc-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jsonrpc-avaje-jsonb diff --git a/modules/jooby-jsonrpc-jackson2/pom.xml b/modules/jooby-jsonrpc-jackson2/pom.xml index dce31fb33a..b0ba563e33 100644 --- a/modules/jooby-jsonrpc-jackson2/pom.xml +++ b/modules/jooby-jsonrpc-jackson2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jsonrpc-jackson2 diff --git a/modules/jooby-jsonrpc-jackson3/pom.xml b/modules/jooby-jsonrpc-jackson3/pom.xml index a524f52ddf..f98d0620f4 100644 --- a/modules/jooby-jsonrpc-jackson3/pom.xml +++ b/modules/jooby-jsonrpc-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jsonrpc-jackson3 diff --git a/modules/jooby-jsonrpc/pom.xml b/modules/jooby-jsonrpc/pom.xml index cc9bf1f728..db85c37da0 100644 --- a/modules/jooby-jsonrpc/pom.xml +++ b/modules/jooby-jsonrpc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jsonrpc diff --git a/modules/jooby-jstachio/pom.xml b/modules/jooby-jstachio/pom.xml index 1e5b8ed430..6008b3152a 100644 --- a/modules/jooby-jstachio/pom.xml +++ b/modules/jooby-jstachio/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jstachio jooby-jstachio diff --git a/modules/jooby-jte/pom.xml b/modules/jooby-jte/pom.xml index b7d1a9f1f1..140161c264 100644 --- a/modules/jooby-jte/pom.xml +++ b/modules/jooby-jte/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jte jooby-jte diff --git a/modules/jooby-jwt/pom.xml b/modules/jooby-jwt/pom.xml index b7fcccaf11..679a5332d0 100644 --- a/modules/jooby-jwt/pom.xml +++ b/modules/jooby-jwt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-jwt jooby-jwt diff --git a/modules/jooby-kafka/pom.xml b/modules/jooby-kafka/pom.xml index e17451a1cb..3513b66ff7 100644 --- a/modules/jooby-kafka/pom.xml +++ b/modules/jooby-kafka/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-kafka jooby-kafka diff --git a/modules/jooby-kotlin/pom.xml b/modules/jooby-kotlin/pom.xml index 5ca39d9ac5..0d32271bda 100644 --- a/modules/jooby-kotlin/pom.xml +++ b/modules/jooby-kotlin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-kotlin jooby-kotlin diff --git a/modules/jooby-langchain4j/pom.xml b/modules/jooby-langchain4j/pom.xml index bdc2b25791..188b9da156 100644 --- a/modules/jooby-langchain4j/pom.xml +++ b/modules/jooby-langchain4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-langchain4j jooby-langchain4j diff --git a/modules/jooby-log4j/pom.xml b/modules/jooby-log4j/pom.xml index 761c804174..439a973337 100644 --- a/modules/jooby-log4j/pom.xml +++ b/modules/jooby-log4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-log4j jooby-log4j diff --git a/modules/jooby-logback/pom.xml b/modules/jooby-logback/pom.xml index 294aada900..b14ae0878e 100644 --- a/modules/jooby-logback/pom.xml +++ b/modules/jooby-logback/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-logback jooby-logback diff --git a/modules/jooby-maven-plugin/pom.xml b/modules/jooby-maven-plugin/pom.xml index 42f5f394fa..9e8b59645b 100644 --- a/modules/jooby-maven-plugin/pom.xml +++ b/modules/jooby-maven-plugin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-maven-plugin jooby-maven-plugin diff --git a/modules/jooby-mcp-jackson2/pom.xml b/modules/jooby-mcp-jackson2/pom.xml index a57ab70a00..b821e9e5db 100644 --- a/modules/jooby-mcp-jackson2/pom.xml +++ b/modules/jooby-mcp-jackson2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-mcp-jackson2 diff --git a/modules/jooby-mcp-jackson3/pom.xml b/modules/jooby-mcp-jackson3/pom.xml index 5b38368211..b4cdd532da 100644 --- a/modules/jooby-mcp-jackson3/pom.xml +++ b/modules/jooby-mcp-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-mcp-jackson3 diff --git a/modules/jooby-mcp/pom.xml b/modules/jooby-mcp/pom.xml index fb64abc89f..b4cc7e905c 100644 --- a/modules/jooby-mcp/pom.xml +++ b/modules/jooby-mcp/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-mcp diff --git a/modules/jooby-metrics/pom.xml b/modules/jooby-metrics/pom.xml index acf9f62932..e4688c0c0c 100644 --- a/modules/jooby-metrics/pom.xml +++ b/modules/jooby-metrics/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-metrics jooby-metrics diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index a4fdc72090..39559439e6 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-mutiny jooby-mutiny diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index 727b659e10..0cff3f91f1 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-netty jooby-netty diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index e96a59758b..a4595ad796 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-openapi jooby-openapi diff --git a/modules/jooby-pac4j/pom.xml b/modules/jooby-pac4j/pom.xml index 6c2ce0a1c5..84daf04744 100644 --- a/modules/jooby-pac4j/pom.xml +++ b/modules/jooby-pac4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-pac4j jooby-pac4j diff --git a/modules/jooby-pebble/pom.xml b/modules/jooby-pebble/pom.xml index 4110eeca4a..b41583c653 100644 --- a/modules/jooby-pebble/pom.xml +++ b/modules/jooby-pebble/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-pebble jooby-pebble diff --git a/modules/jooby-quartz/pom.xml b/modules/jooby-quartz/pom.xml index b8d4002f57..de0709f41e 100644 --- a/modules/jooby-quartz/pom.xml +++ b/modules/jooby-quartz/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-quartz jooby-quartz diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index 5062931c00..15422995ac 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-reactor jooby-reactor diff --git a/modules/jooby-redis/pom.xml b/modules/jooby-redis/pom.xml index 4be8db1bb7..385cafbed2 100644 --- a/modules/jooby-redis/pom.xml +++ b/modules/jooby-redis/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-redis jooby-redis diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index 6a618a4f7d..18d8f46e77 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-redoc jooby-redoc diff --git a/modules/jooby-rocker/pom.xml b/modules/jooby-rocker/pom.xml index 8807f4c631..f7be523ad5 100644 --- a/modules/jooby-rocker/pom.xml +++ b/modules/jooby-rocker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-rocker jooby-rocker diff --git a/modules/jooby-run/pom.xml b/modules/jooby-run/pom.xml index af70d299f3..3df890b956 100644 --- a/modules/jooby-run/pom.xml +++ b/modules/jooby-run/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-run jooby-run diff --git a/modules/jooby-rxjava3/pom.xml b/modules/jooby-rxjava3/pom.xml index 925aacbb3e..92b447eb82 100644 --- a/modules/jooby-rxjava3/pom.xml +++ b/modules/jooby-rxjava3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-rxjava3 jooby-rxjava3 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index b51a798935..76855c8cd2 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -4,7 +4,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-stork diff --git a/modules/jooby-swagger-ui/pom.xml b/modules/jooby-swagger-ui/pom.xml index 91e14d1f8a..a7543ba1de 100644 --- a/modules/jooby-swagger-ui/pom.xml +++ b/modules/jooby-swagger-ui/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-swagger-ui jooby-swagger-ui diff --git a/modules/jooby-test/pom.xml b/modules/jooby-test/pom.xml index 4bce67527f..13ea00f072 100644 --- a/modules/jooby-test/pom.xml +++ b/modules/jooby-test/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-test jooby-test diff --git a/modules/jooby-thymeleaf/pom.xml b/modules/jooby-thymeleaf/pom.xml index a6b0b57748..2ab4772c7e 100644 --- a/modules/jooby-thymeleaf/pom.xml +++ b/modules/jooby-thymeleaf/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-thymeleaf jooby-thymeleaf diff --git a/modules/jooby-trpc-avaje-jsonb/pom.xml b/modules/jooby-trpc-avaje-jsonb/pom.xml index 30c6ec39c8..6876740527 100644 --- a/modules/jooby-trpc-avaje-jsonb/pom.xml +++ b/modules/jooby-trpc-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-trpc-avaje-jsonb diff --git a/modules/jooby-trpc-generator/pom.xml b/modules/jooby-trpc-generator/pom.xml index 57dd67196c..09a6d1a209 100644 --- a/modules/jooby-trpc-generator/pom.xml +++ b/modules/jooby-trpc-generator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-trpc-generator jooby-trpc-generator diff --git a/modules/jooby-trpc-jackson2/pom.xml b/modules/jooby-trpc-jackson2/pom.xml index 16ef9ce522..d33a147d1e 100644 --- a/modules/jooby-trpc-jackson2/pom.xml +++ b/modules/jooby-trpc-jackson2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-trpc-jackson2 diff --git a/modules/jooby-trpc-jackson3/pom.xml b/modules/jooby-trpc-jackson3/pom.xml index 2ae4b016b2..1d67266a9f 100644 --- a/modules/jooby-trpc-jackson3/pom.xml +++ b/modules/jooby-trpc-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-trpc-jackson3 diff --git a/modules/jooby-trpc/pom.xml b/modules/jooby-trpc/pom.xml index 2981877869..fb60b5cecb 100644 --- a/modules/jooby-trpc/pom.xml +++ b/modules/jooby-trpc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-trpc jooby-trpc diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index c53ba9e090..50af502501 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-undertow jooby-undertow diff --git a/modules/jooby-vertx-mysql-client/pom.xml b/modules/jooby-vertx-mysql-client/pom.xml index 25b508d96f..99fdaaba4e 100644 --- a/modules/jooby-vertx-mysql-client/pom.xml +++ b/modules/jooby-vertx-mysql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-vertx-mysql-client jooby-vertx-mysql-client diff --git a/modules/jooby-vertx-pg-client/pom.xml b/modules/jooby-vertx-pg-client/pom.xml index 0dc52d225f..04073fb53b 100644 --- a/modules/jooby-vertx-pg-client/pom.xml +++ b/modules/jooby-vertx-pg-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-vertx-pg-client jooby-vertx-pg-client diff --git a/modules/jooby-vertx-sql-client/pom.xml b/modules/jooby-vertx-sql-client/pom.xml index 701e77e841..81d4d45b1f 100644 --- a/modules/jooby-vertx-sql-client/pom.xml +++ b/modules/jooby-vertx-sql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-vertx-sql-client jooby-vertx-sql-client diff --git a/modules/jooby-vertx/pom.xml b/modules/jooby-vertx/pom.xml index 5a431a84c6..31da69a8d7 100644 --- a/modules/jooby-vertx/pom.xml +++ b/modules/jooby-vertx/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-vertx jooby-vertx diff --git a/modules/jooby-whoops/pom.xml b/modules/jooby-whoops/pom.xml index efe96f17fa..6dd4e218c7 100644 --- a/modules/jooby-whoops/pom.xml +++ b/modules/jooby-whoops/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-whoops jooby-whoops diff --git a/modules/jooby-yasson/pom.xml b/modules/jooby-yasson/pom.xml index 8f7ba189c1..66b85f5856 100644 --- a/modules/jooby-yasson/pom.xml +++ b/modules/jooby-yasson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.0 + 4.3.1-SNAPSHOT jooby-yasson jooby-yasson diff --git a/modules/pom.xml b/modules/pom.xml index 204d443c10..407359fb36 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ io.jooby jooby-project - 4.3.0 + 4.3.1-SNAPSHOT modules diff --git a/pom.xml b/pom.xml index edc632352b..3e0a8abcfd 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jooby jooby-project - 4.3.0 + 4.3.1-SNAPSHOT pom jooby-project @@ -212,7 +212,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2026-04-08T14:58:58Z + 2026-04-08T17:05:01Z UTF-8 etc${file.separator}source${file.separator}formatter.sh diff --git a/tests/pom.xml b/tests/pom.xml index 843e04465f..042f426878 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.3.0 + 4.3.1-SNAPSHOT tests tests From fdbbc8cae2ec5f014ea8b1ad4ec62965265201d5 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 8 Apr 2026 14:15:05 -0300 Subject: [PATCH 02/11] build: simplify GitHub Action build --- .github/workflows/full-build.yml | 8 ++++---- .github/workflows/quick-build.yml | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.github/workflows/full-build.yml b/.github/workflows/full-build.yml index 31c1ec5791..0e8ad03e07 100644 --- a/.github/workflows/full-build.yml +++ b/.github/workflows/full-build.yml @@ -4,6 +4,8 @@ on: push: branches: - main # Only run push on main (merges/direct pushes) + tags: + - 'v*' # Triggers on any tag starting with 'v' (e.g., v1.0, v2.1.3) pull_request: branches: - main # Run on any PR targeting main @@ -27,15 +29,13 @@ jobs: java-version: ${{ matrix.java-version }} distribution: 'temurin' cache: maven - - name: Install - run: mvn clean install -DskipTests -q -P gradlePlugin - name: Build - run: mvn -B package -P gradlePlugin + run: mvn -B install -P gradlePlugin --no-transfer-progress env: BUILD_LOG_LEVEL: 'ERROR' - name: Tests uses: mikepenz/action-junit-report@v5 - if: failure() + if: always() with: check_name: Test ${{ matrix.os }} ${{ matrix.java-version }} report_paths: '*/target/*/TEST-*.xml' diff --git a/.github/workflows/quick-build.yml b/.github/workflows/quick-build.yml index bb303ed988..944c69a7ca 100644 --- a/.github/workflows/quick-build.yml +++ b/.github/workflows/quick-build.yml @@ -24,19 +24,15 @@ jobs: java-version: ${{ matrix.java_version }} distribution: 'temurin' cache: maven - - name: Install - run: mvn install -DskipTests -q -B - env: - BUILD_LOG_LEVEL: 'ERROR' - name: Build - run: mvn package + run: mvn -B install -P gradlePlugin --no-transfer-progress env: BUILD_PORT: 0 BUILD_SECURE_PORT: 0 BUILD_LOG_LEVEL: 'ERROR' - name: Test Result uses: mikepenz/action-junit-report@v5 - if: failure() + if: always() with: check_name: JUnit ${{ matrix.kind }} ${{ matrix.java_version }} ${{ matrix.os }} report_paths: '*/target/*/TEST-*.xml' From 3d43cddcea01599e7b23fa60c85ce460ac101220 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Wed, 8 Apr 2026 18:38:36 -0300 Subject: [PATCH 03/11] build: add deprecated section to release notes --- .github/workflows/maven-central.yml | 47 ++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/maven-central.yml b/.github/workflows/maven-central.yml index 8b9af8dbf9..faf632cf7b 100644 --- a/.github/workflows/maven-central.yml +++ b/.github/workflows/maven-central.yml @@ -78,31 +78,40 @@ jobs: --search "milestone:\"$VERSION_NUM\" label:break-change" \ --state all \ --limit 100 \ - --json title,url \ - --jq '.[] | "- [\(.title)](\(.url))"' > breaking_issues.md + --json title,number \ + --jq '.[] | "- \(.title) #\(.number)"' > breaking_issues.md - # 2. Fetch NEW features (requires the 'feature' label) + # 2. Fetch NEW features (requires 'feature', excludes 'break-change') gh issue list \ --repo ${{ github.repository }} \ --search "milestone:\"$VERSION_NUM\" label:feature -label:break-change" \ --state all \ --limit 100 \ - --json title,url \ - --jq '.[] | "- [\(.title)](\(.url))"' > new_issues.md + --json title,number \ + --jq '.[] | "- \(.title) #\(.number)"' > new_issues.md - # 3. Fetch OTHER changes (excludes 'feature', 'break-change', and 'dependencies') + # 3. Fetch DEPRECATED changes (requires 'deprecated', excludes 'break-change') gh issue list \ --repo ${{ github.repository }} \ - --search "milestone:\"$VERSION_NUM\" -label:feature -label:break-change -label:dependencies" \ + --search "milestone:\"$VERSION_NUM\" label:deprecated -label:break-change" \ --state all \ --limit 100 \ - --json title,url \ - --jq '.[] | "- [\(.title)](\(.url))"' > other_issues.md + --json title,number \ + --jq '.[] | "- \(.title) #\(.number)"' > deprecated_issues.md - # 4. Initialize the changelog file + # 4. Fetch OTHER changes (excludes 'feature', 'break-change', 'deprecated', and 'dependencies') + gh issue list \ + --repo ${{ github.repository }} \ + --search "milestone:\"$VERSION_NUM\" -label:feature -label:break-change -label:deprecated -label:dependencies" \ + --state all \ + --limit 100 \ + --json title,number \ + --jq '.[] | "- \(.title) #\(.number)"' > other_issues.md + + # 5. Initialize the changelog file > changelog.md - # 5. Conditionally add "Breaking Changes" if breaking_issues.md has content (-s) + # 6. Conditionally add "Breaking Changes" if [ -s breaking_issues.md ]; then echo "## ⚠️ Breaking Changes" >> changelog.md echo "" >> changelog.md @@ -110,7 +119,7 @@ jobs: echo "" >> changelog.md fi - # 6. Conditionally add "What's New" if new_issues.md has content + # 7. Conditionally add "What's New" if [ -s new_issues.md ]; then echo "## 🚀 What's New" >> changelog.md echo "" >> changelog.md @@ -118,7 +127,7 @@ jobs: echo "" >> changelog.md fi - # 7. Conditionally add "Other Changes" if other_issues.md has content + # 8. Conditionally add "Other Changes" if [ -s other_issues.md ]; then echo "## 🛠️ Changes" >> changelog.md echo "" >> changelog.md @@ -126,9 +135,17 @@ jobs: echo "" >> changelog.md fi - # 8. Build the rest of the changelog (Links & Sponsors) + # 9. Conditionally add "Deprecated" + if [ -s deprecated_issues.md ]; then + echo "## 🗑️ Deprecated" >> changelog.md + echo "" >> changelog.md + cat deprecated_issues.md >> changelog.md + echo "" >> changelog.md + fi + + # 10. Build the rest of the changelog (Links & Sponsors) cat << EOF >> changelog.md - ### 🔗 Links & Resources + ## 🔗 Links & Resources - [$CURRENT_TAG](https://github.com/jooby-project/jooby/tree/$CURRENT_TAG) - [Closed Issues](https://github.com/jooby-project/jooby/milestone/$MILESTONE_ID?closed=1) - [Changelog](https://github.com/jooby-project/jooby/compare/$PREV_TAG...$CURRENT_TAG) From 5c0f20eeecba75fbc024d2841b4e2c833e92b171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:02:59 +0000 Subject: [PATCH 04/11] build(deps): bump swagger-ui-dist in /modules/jooby-swagger-ui Bumps [swagger-ui-dist](https://github.com/swagger-api/swagger-ui) from 5.32.1 to 5.32.2. - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.32.1...v5.32.2) --- updated-dependencies: - dependency-name: swagger-ui-dist dependency-version: 5.32.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- modules/jooby-swagger-ui/package-lock.json | 8 ++++---- modules/jooby-swagger-ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/jooby-swagger-ui/package-lock.json b/modules/jooby-swagger-ui/package-lock.json index b4f9a409c1..365638786a 100644 --- a/modules/jooby-swagger-ui/package-lock.json +++ b/modules/jooby-swagger-ui/package-lock.json @@ -9,7 +9,7 @@ "version": "4.0.0", "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.32.1" + "swagger-ui-dist": "^5.32.2" } }, "node_modules/@scarf/scarf": { @@ -20,9 +20,9 @@ "license": "Apache-2.0" }, "node_modules/swagger-ui-dist": { - "version": "5.32.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.1.tgz", - "integrity": "sha512-6HQoo7+j8PA2QqP5kgAb9dl1uxUjvR0SAoL/WUp1sTEvm0F6D5npgU2OGCLwl++bIInqGlEUQ2mpuZRZYtyCzQ==", + "version": "5.32.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.2.tgz", + "integrity": "sha512-t6Ns52nS8LU2hqi0+rezMjFO1ZrCsCrnommXrU7Nfrg2va2dWahdvM6TuSwzdHpG29v6BHJyU1c/UWFhgVZzVQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/modules/jooby-swagger-ui/package.json b/modules/jooby-swagger-ui/package.json index 3b57b4d361..04578b2c3f 100644 --- a/modules/jooby-swagger-ui/package.json +++ b/modules/jooby-swagger-ui/package.json @@ -4,7 +4,7 @@ "private": true, "license": "ASF", "dependencies": { - "swagger-ui-dist": "^5.32.1" + "swagger-ui-dist": "^5.32.2" }, "scarfSettings": { "enabled": false From 0170fae67749afea98999f2113186abe41fd8a2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:10:13 +0000 Subject: [PATCH 05/11] build(deps): bump the dependencies group with 20 updates Bumps the dependencies group with 20 updates: | Package | From | To | | --- | --- | --- | | [tools.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) | `3.1.1` | `3.1.2` | | [io.avaje:avaje-inject](https://github.com/avaje/avaje-inject) | `12.4` | `12.5` | | io.avaje:avaje-inject-generator | `12.4` | `12.5` | | [io.avaje:avaje-jsonb](https://github.com/avaje/avaje-jsonb) | `3.11` | `3.12` | | io.avaje:avaje-jsonb-generator | `3.11` | `3.12` | | org.thymeleaf:thymeleaf | `3.1.3.RELEASE` | `3.1.4.RELEASE` | | io.swagger.core.v3:swagger-annotations | `2.2.46` | `2.2.47` | | io.swagger.core.v3:swagger-models | `2.2.46` | `2.2.47` | | [io.ebean:ebean](https://github.com/ebean-orm/ebean) | `17.3.0` | `17.5.0` | | [io.ebean:ebean-querybean](https://github.com/ebean-orm/ebean) | `17.3.0` | `17.5.0` | | [io.ebean:querybean-generator](https://github.com/ebean-orm/ebean) | `17.3.0` | `17.5.0` | | [io.ebean:ebean-test](https://github.com/ebean-orm/ebean) | `17.3.0` | `17.5.0` | | [org.jdbi:jdbi3-core](https://github.com/jdbi/jdbi) | `3.52.0` | `3.52.1` | | [com.bucket4j:bucket4j_jdk17-core](https://github.com/bucket4j/bucket4j) | `8.17.0` | `8.18.0` | | org.apache.ant:ant | `1.10.16` | `1.10.17` | | [dev.langchain4j:langchain4j-bom](https://github.com/langchain4j/langchain4j) | `1.12.2` | `1.13.0` | | [cz.habarta.typescript-generator:typescript-generator-core](https://github.com/vojtechhabarta/typescript-generator) | `3.2.1263` | `4.0.0` | | software.amazon.awssdk:bom | `2.42.28` | `2.42.33` | | [com.fizzed:stork-maven-plugin](https://github.com/fizzed/stork) | `3.2.0` | `3.3.0` | | [org.asynchttpclient:async-http-client](https://github.com/AsyncHttpClient/async-http-client) | `3.0.8` | `3.0.9` | Updates `tools.jackson:jackson-bom` from 3.1.1 to 3.1.2 - [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-3.1.1...jackson-bom-3.1.2) Updates `io.avaje:avaje-inject` from 12.4 to 12.5 - [Release notes](https://github.com/avaje/avaje-inject/releases) - [Commits](https://github.com/avaje/avaje-inject/compare/12.4...12.5) Updates `io.avaje:avaje-inject-generator` from 12.4 to 12.5 Updates `io.avaje:avaje-inject-generator` from 12.4 to 12.5 Updates `io.avaje:avaje-jsonb` from 3.11 to 3.12 - [Release notes](https://github.com/avaje/avaje-jsonb/releases) - [Commits](https://github.com/avaje/avaje-jsonb/compare/3.11...3.12) Updates `io.avaje:avaje-jsonb-generator` from 3.11 to 3.12 Updates `io.avaje:avaje-jsonb-generator` from 3.11 to 3.12 Updates `org.thymeleaf:thymeleaf` from 3.1.3.RELEASE to 3.1.4.RELEASE Updates `io.swagger.core.v3:swagger-annotations` from 2.2.46 to 2.2.47 Updates `io.swagger.core.v3:swagger-models` from 2.2.46 to 2.2.47 Updates `io.swagger.core.v3:swagger-models` from 2.2.46 to 2.2.47 Updates `io.ebean:ebean` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-querybean` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:querybean-generator` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-test` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-querybean` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:querybean-generator` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `io.ebean:ebean-test` from 17.3.0 to 17.5.0 - [Release notes](https://github.com/ebean-orm/ebean/releases) - [Commits](https://github.com/ebean-orm/ebean/commits) Updates `org.jdbi:jdbi3-core` from 3.52.0 to 3.52.1 - [Release notes](https://github.com/jdbi/jdbi/releases) - [Changelog](https://github.com/jdbi/jdbi/blob/master/RELEASE_NOTES.md) - [Commits](https://github.com/jdbi/jdbi/compare/v3.52.0...v3.52.1) Updates `com.bucket4j:bucket4j_jdk17-core` from 8.17.0 to 8.18.0 - [Release notes](https://github.com/bucket4j/bucket4j/releases) - [Commits](https://github.com/bucket4j/bucket4j/compare/8.17.0...8.18.0) Updates `org.apache.ant:ant` from 1.10.16 to 1.10.17 Updates `dev.langchain4j:langchain4j-bom` from 1.12.2 to 1.13.0 - [Release notes](https://github.com/langchain4j/langchain4j/releases) - [Commits](https://github.com/langchain4j/langchain4j/compare/1.12.2...1.13.0) Updates `cz.habarta.typescript-generator:typescript-generator-core` from 3.2.1263 to 4.0.0 - [Release notes](https://github.com/vojtechhabarta/typescript-generator/releases) - [Commits](https://github.com/vojtechhabarta/typescript-generator/compare/v3.2.1263...v4.0.0) Updates `software.amazon.awssdk:bom` from 2.42.28 to 2.42.33 Updates `com.fizzed:stork-maven-plugin` from 3.2.0 to 3.3.0 - [Changelog](https://github.com/fizzed/stork/blob/master/CHANGELOG.md) - [Commits](https://github.com/fizzed/stork/compare/v3.2.0...v3.3.0) Updates `org.asynchttpclient:async-http-client` from 3.0.8 to 3.0.9 - [Release notes](https://github.com/AsyncHttpClient/async-http-client/releases) - [Commits](https://github.com/AsyncHttpClient/async-http-client/compare/async-http-client-project-3.0.8...async-http-client-project-3.0.9) --- updated-dependencies: - dependency-name: tools.jackson:jackson-bom dependency-version: 3.1.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.avaje:avaje-inject dependency-version: '12.5' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.5' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-inject-generator dependency-version: '12.5' dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb dependency-version: '3.12' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.12' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.avaje:avaje-jsonb-generator dependency-version: '3.12' dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.thymeleaf:thymeleaf dependency-version: 3.1.4.RELEASE dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-annotations dependency-version: 2.2.47 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.47 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.swagger.core.v3:swagger-models dependency-version: 2.2.47 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: io.ebean:ebean dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-querybean dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:querybean-generator dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-test dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-querybean dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:querybean-generator dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: io.ebean:ebean-test dependency-version: 17.5.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.jdbi:jdbi3-core dependency-version: 3.52.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.bucket4j:bucket4j_jdk17-core dependency-version: 8.18.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.apache.ant:ant dependency-version: 1.10.17 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: dev.langchain4j:langchain4j-bom dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: cz.habarta.typescript-generator:typescript-generator-core dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: dependencies - dependency-name: software.amazon.awssdk:bom dependency-version: 2.42.33 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies - dependency-name: com.fizzed:stork-maven-plugin dependency-version: 3.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dependencies - dependency-name: org.asynchttpclient:async-http-client dependency-version: 3.0.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dependencies ... Signed-off-by: dependabot[bot] --- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-langchain4j/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-trpc-generator/pom.xml | 2 +- pom.xml | 20 ++++++++++---------- tests/pom.xml | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index 632ca647eb..ba0fd7ec6d 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -12,7 +12,7 @@ jooby-awssdk-v2 - 2.42.28 + 2.42.33 diff --git a/modules/jooby-langchain4j/pom.xml b/modules/jooby-langchain4j/pom.xml index 188b9da156..013df74c96 100644 --- a/modules/jooby-langchain4j/pom.xml +++ b/modules/jooby-langchain4j/pom.xml @@ -73,7 +73,7 @@ dev.langchain4j langchain4j-bom - 1.12.2 + 1.13.0 pom import diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index a4595ad796..25b5de170d 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -110,7 +110,7 @@ io.avaje avaje-inject - 12.4 + 12.5 test diff --git a/modules/jooby-trpc-generator/pom.xml b/modules/jooby-trpc-generator/pom.xml index 09a6d1a209..995be1547c 100644 --- a/modules/jooby-trpc-generator/pom.xml +++ b/modules/jooby-trpc-generator/pom.xml @@ -28,7 +28,7 @@ cz.habarta.typescript-generator typescript-generator-core - 3.2.1263 + 4.0.0 diff --git a/pom.xml b/pom.xml index 3e0a8abcfd..6d4a04fb80 100644 --- a/pom.xml +++ b/pom.xml @@ -62,20 +62,20 @@ 1.3.7 4.1.1 2.21.2 - 3.1.1 + 3.1.2 2.13.2 3.0.1 3.0.4 2.4.0 - 3.1.3.RELEASE + 3.1.4.RELEASE 3.2.3 7.0.2 1.2 7.0.4.Final - 17.3.0 - 3.52.0 + 17.5.0 + 3.52.1 11.20.1 25.0 7.5.1.RELEASE @@ -113,7 +113,7 @@ 5.0.10 - 2.2.46 + 2.2.47 2.1.39 2.0.0-rc.20 @@ -121,8 +121,8 @@ 2.5.2 - 12.4 - 3.11 + 12.5 + 3.12 2.17 @@ -137,7 +137,7 @@ 6.4.0 2.5.2 9.2.1 - 8.17.0 + 8.18.0 1.12.797 4.18.1 1.9.3 @@ -192,7 +192,7 @@ 3.5.5 2.3.1 4.0.3 - 3.2.0 + 3.3.0 2.21.0 3.6.3 3.1.2 @@ -1342,7 +1342,7 @@ org.apache.ant ant - 1.10.16 + 1.10.17 diff --git a/tests/pom.xml b/tests/pom.xml index 042f426878..f7b794b32f 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -334,7 +334,7 @@ org.asynchttpclient async-http-client - 3.0.8 + 3.0.9 From a1cede76d189533b6deb0ec8d8659a4ed1f9bba6 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 13 Apr 2026 18:39:11 -0300 Subject: [PATCH 06/11] feat(opentelemetry): introduce comprehensive OpenTelemetry module and instrumentations This commit introduces the foundational `OtelModule` and a suite of native instrumentations to seamlessly integrate OpenTelemetry tracing, metrics, and logging into Jooby applications. Core features: - Add `OtelModule` to bootstrap the OpenTelemetry SDK. - Add `OtelHttpTracing` filter for automated HTTP route tracing with W3C propagation. - Add `Trace` utility with fluent API for safe, manual service-layer instrumentation. - Add `OtelServerMetrics` to export native operational metrics for Netty, Jetty, and Undertow. Third-party extensions: - Add `OtelHikari` for database connection pool metrics. - Add `OtelLogback` and `OtelLog4j2` for automatic trace correlation in application logs. - Add `OtelQuartz` and `OtelDbScheduler` for background job observability. --- jooby/src/main/java/io/jooby/Jooby.java | 4 +- .../java/io/jooby/internal/RouterImpl.java | 2 +- modules/jooby-bom/pom.xml | 5 + modules/jooby-db-scheduler/pom.xml | 2 +- .../jooby/dbscheduler/DbSchedulerModule.java | 17 +- .../java/io/jooby/hikari/HikariModule.java | 228 +++++++++----- .../main/java/io/jooby/jetty/JettyServer.java | 14 +- .../netty/NettyEventLoopGroupImpl.java | 2 +- .../main/java/io/jooby/netty/NettyServer.java | 11 +- modules/jooby-opentelemetry/pom.xml | 176 +++++++++++ .../io/jooby/opentelemetry/OtelExtension.java | 36 +++ .../jooby/opentelemetry/OtelHttpTracing.java | 127 ++++++++ .../io/jooby/opentelemetry/OtelModule.java | 231 ++++++++++++++ .../java/io/jooby/opentelemetry/Trace.java | 128 ++++++++ .../instrumentation/OtelDbScheduler.java | 154 +++++++++ .../instrumentation/OtelHikari.java | 70 +++++ .../instrumentation/OtelLog4j2.java | 86 +++++ .../instrumentation/OtelLogback.java | 86 +++++ .../instrumentation/OtelQuartz.java | 68 ++++ .../instrumentation/OtelServerMetrics.java | 296 ++++++++++++++++++ .../io/jooby/opentelemetry/package-info.java | 105 +++++++ .../src/main/java/module-info.java | 152 +++++++++ .../opentelemetry/OtelHttpTracingTest.java | 195 ++++++++++++ .../jooby/opentelemetry/OtelModuleTest.java | 87 +++++ .../io/jooby/opentelemetry/TraceTest.java | 184 +++++++++++ .../instrumentation/OtelDbSchedulerTest.java | 171 ++++++++++ .../instrumentation/OtelHikariTest.java | 74 +++++ .../instrumentation/OtelLog4j2Test.java | 119 +++++++ .../instrumentation/OtelLogbackTest.java | 102 ++++++ .../instrumentation/OtelQuartzTest.java | 64 ++++ .../OtelServerMetricsTest.java | 224 +++++++++++++ .../io/jooby/undertow/UndertowServer.java | 6 +- modules/pom.xml | 5 +- pom.xml | 1 + tests/pom.xml | 6 + 35 files changed, 3149 insertions(+), 89 deletions(-) create mode 100644 modules/jooby-opentelemetry/pom.xml create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelExtension.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelHttpTracing.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelModule.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/Trace.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelDbScheduler.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelHikari.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLogback.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelQuartz.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelServerMetrics.java create mode 100644 modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/package-info.java create mode 100644 modules/jooby-opentelemetry/src/main/java/module-info.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelHttpTracingTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelModuleTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/TraceTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelDbSchedulerTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelHikariTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2Test.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLogbackTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelQuartzTest.java create mode 100644 modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelServerMetricsTest.java diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index c0bcb5ce2b..3b24254776 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -937,7 +937,7 @@ public Jooby start(@NonNull Server server) { router.initialize(); - for (Extension extension : lateExtensions) { + for (var extension : lateExtensions) { try { extension.install(this); } catch (Throwable e) { @@ -949,7 +949,7 @@ public Jooby start(@NonNull Server server) { this.startingCallbacks = fire(this.startingCallbacks); - router.start(this, server); + router.start(this); return this; } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 63c036bacc..ee50a105f9 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -548,7 +548,7 @@ public void initialize() { configureContextAsService(routerOptions.isContextAsService()); } - @NonNull public Router start(@NonNull Jooby app, @NonNull Server server) { + @NonNull public Router start(@NonNull Jooby app) { started = true; var globalErrHandler = defineGlobalErrorHandler(app); if (err == null) { diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index d4e797270a..99d5674bd2 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -275,6 +275,11 @@ jooby-openapi ${project.version} + + io.jooby + jooby-opentelemetry + ${project.version} + io.jooby jooby-pac4j diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index 3e4e531635..bbdb059bf5 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -22,7 +22,7 @@ com.github.kagkarlsson db-scheduler - 16.7.1 + ${db-scheduler.version} diff --git a/modules/jooby-db-scheduler/src/main/java/io/jooby/dbscheduler/DbSchedulerModule.java b/modules/jooby-db-scheduler/src/main/java/io/jooby/dbscheduler/DbSchedulerModule.java index d83f33c99d..f6dd32b2e8 100644 --- a/modules/jooby-db-scheduler/src/main/java/io/jooby/dbscheduler/DbSchedulerModule.java +++ b/modules/jooby-db-scheduler/src/main/java/io/jooby/dbscheduler/DbSchedulerModule.java @@ -20,6 +20,7 @@ import com.github.kagkarlsson.scheduler.Scheduler; import com.github.kagkarlsson.scheduler.SchedulerName; +import com.github.kagkarlsson.scheduler.event.ExecutionInterceptor; import com.github.kagkarlsson.scheduler.jdbc.AutodetectJdbcCustomization; import com.github.kagkarlsson.scheduler.jdbc.JdbcCustomization; import com.github.kagkarlsson.scheduler.serializer.Serializer; @@ -73,6 +74,7 @@ public class DbSchedulerModule implements Extension { private ExecutorService dueExecutor; private ScheduledExecutorService housekeeperExecutor; private JdbcCustomization jdbcCustomization; + private final List executionInterceptors = new ArrayList<>(); /** * Creates a new module. @@ -126,6 +128,18 @@ public DbSchedulerModule withSchedulerName(@NonNull SchedulerName schedulerName) return this; } + /** + * Adds an execution interceptor to the scheduler module. Execution interceptors are used to + * customize the behavior of task execution, such as logging, monitoring, or modifying tasks. + * + * @param interceptor An {@link ExecutionInterceptor} that intercepts task execution. + * @return This {@link DbSchedulerModule} to allow method chaining. + */ + public DbSchedulerModule withExecutionInterceptor(@NonNull ExecutionInterceptor interceptor) { + this.executionInterceptors.add(interceptor); + return this; + } + /** * Set Task serializer. * @@ -280,7 +294,8 @@ public void install(@NonNull Jooby app) throws SQLException { // schedulerListeners.forEach(builder::addSchedulerListener); // Register interceptors - // executionInterceptors.forEach(builder::addExecutionInterceptor); + executionInterceptors.forEach(builder::addExecutionInterceptor); + var scheduler = builder.build(); app.getServices().put(Scheduler.class, scheduler); diff --git a/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java b/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java index 3cd694352e..29e851d15f 100644 --- a/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java +++ b/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java @@ -213,13 +213,16 @@ public void install(@NonNull Jooby application) { ServiceRegistry registry = application.getServices(); ServiceKey key = ServiceKey.key(DataSource.class, database); - /** Global default database: */ + /* Global default database: */ registry.putIfAbsent(KEY, dataSource); - /** Specific access: */ + /* Specific access: */ registry.put(key, dataSource); + /* List access: */ + registry.listOf(DataSource.class).add(dataSource); + registry.listOf(HikariDataSource.class).add(dataSource); - application.onStop(dataSource::close); + application.onStop(dataSource); } /** @@ -231,13 +234,11 @@ public void install(@NonNull Jooby application) { * @param url Jdbc connection string (a.k.a jdbc url) * @return Database type or given jdbc connection string for unknown or bad urls. */ - public static @NonNull String databaseType(@NonNull String url) { - String type = - Arrays.stream(url.toLowerCase().split(":")) - .filter(token -> !SKIP_TOKENS.contains(token)) - .findFirst() - .orElse(url); - return type; + public static String databaseType(@NonNull String url) { + return Arrays.stream(url.toLowerCase().split(":")) + .filter(token -> !SKIP_TOKENS.contains(token)) + .findFirst() + .orElse(url); } /** @@ -288,71 +289,151 @@ private static Map defaults(String database, Environment env) { defaults.put( "maximumPoolSize", Math.max(MINIMUM_SIZE, Runtime.getRuntime().availableProcessors() * WORKER_FACTOR)); - if ("derby".equals(database)) { - // url => jdbc:derby:${db};create=true - defaults.put("dataSourceClassName", "org.apache.derby.jdbc.ClientDataSource"); - } else if ("db2".equals(database)) { - // url => jdbc:db2://127.0.0.1:50000/SAMPLE - defaults.put("dataSourceClassName", "com.ibm.db2.jcc.DB2SimpleDataSource"); - } else if ("h2".equals(database)) { - // url => mem, fs or jdbc:h2:${db} - defaults.put("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource"); - defaults.put("dataSource.user", "sa"); - defaults.put("dataSource.password", ""); - } else if ("hsqldb".equals(database)) { - // url => jdbc:hsqldb:file:${db} - defaults.put("dataSourceClassName", "org.hsqldb.jdbc.JDBCDataSource"); - } else if ("mariadb".equals(database)) { - // url jdbc:mariadb://:/?=&=... - defaults.put("dataSourceClassName", "org.mariadb.jdbc.MySQLDataSource"); - } else if ("mysql".equals(database)) { - // url jdbc:mysql://:/?=&=... - // 6.x - env.loadClass("com.mysql.cj.jdbc.MysqlDataSource") - .ifPresent(klass -> defaults.put("dataSourceClassName", klass.getName())); - // 5.x - if (!defaults.containsKey("dataSourceClassName")) { - env.loadClass("com.mysql.jdbc.jdbc2.optional.MysqlDataSource") - .ifPresent( - klass -> { - defaults.put("dataSourceClassName", klass.getName()); - defaults.put( - "dataSource.encoding", env.getConfig().getString(AvailableSettings.CHARSET)); - defaults.put("dataSource.cachePrepStmts", true); - defaults.put("dataSource.prepStmtCacheSize", MYSQL5_STT_CACHE_SIZE); - defaults.put("dataSource.prepStmtCacheSqlLimit", MYSQL5_STT_CACHE_SQL_LIMIT); - defaults.put("dataSource.useServerPrepStmts", true); - }); + if (database == null) { + return defaults; + } + switch (database) { + case "derby" -> + // url => jdbc:derby:${db};create=true + defaults.put("dataSourceClassName", "org.apache.derby.jdbc.ClientDataSource"); + case "db2" -> + // url => jdbc:db2://127.0.0.1:50000/SAMPLE + defaults.put("dataSourceClassName", "com.ibm.db2.jcc.DB2SimpleDataSource"); + case "h2" -> { + // url => mem, fs or jdbc:h2:${db} + defaults.put("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource"); + defaults.put("dataSource.user", "sa"); + defaults.put("dataSource.password", ""); + } + case "hsqldb" -> + // url => jdbc:hsqldb:file:${db} + defaults.put("dataSourceClassName", "org.hsqldb.jdbc.JDBCDataSource"); + case "mariadb" -> + // url jdbc:mariadb://:/?=&=... + defaults.put("dataSourceClassName", "org.mariadb.jdbc.MySQLDataSource"); + case "mysql" -> { + // url jdbc:mysql://:/?=&=... + // 6.x + env.loadClass("com.mysql.cj.jdbc.MysqlDataSource") + .ifPresent(klass -> defaults.put("dataSourceClassName", klass.getName())); + // 5.x + if (!defaults.containsKey("dataSourceClassName")) { + env.loadClass("com.mysql.jdbc.jdbc2.optional.MysqlDataSource") + .ifPresent( + klass -> { + defaults.put("dataSourceClassName", klass.getName()); + defaults.put( + "dataSource.encoding", + env.getConfig().getString(AvailableSettings.CHARSET)); + defaults.put("dataSource.cachePrepStmts", true); + defaults.put("dataSource.prepStmtCacheSize", MYSQL5_STT_CACHE_SIZE); + defaults.put("dataSource.prepStmtCacheSqlLimit", MYSQL5_STT_CACHE_SQL_LIMIT); + defaults.put("dataSource.useServerPrepStmts", true); + }); + } } - } else if ("sqlserver".equals(database)) { - // url => - // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] - defaults.put("dataSourceClassName", "com.microsoft.sqlserver.jdbc.SQLServerDataSource"); - } else if ("oracle".equals(database)) { - // url => jdbc:oracle:thin:@//:/ - defaults.put("dataSourceClassName", "oracle.jdbc.pool.OracleDataSource"); - } else if ("pgsql".equals(database)) { - // url => jdbc:pgsql://[:]/ - defaults.put("dataSourceClassName", "com.impossibl.postgres.jdbc.PGDataSource"); - } else if ("postgresql".equals(database)) { - // url => jdbc:postgresql://host:port/database - defaults.put("dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource"); - } else if ("sybase".equals(database)) { - // url => jdbc:jtds:sybase://[:][/] - defaults.put("dataSourceClassName", "com.sybase.jdbcx.SybDataSource"); - } else if ("firebirdsql".equals(database)) { - // jdbc:firebirdsql:host[/port]: - defaults.put("dataSourceClassName", "org.firebirdsql.pool.FBSimpleDataSource"); - } else if ("sqlite".equals(database)) { - // jdbc:sqlite:${db} - defaults.put("dataSourceClassName", "org.sqlite.SQLiteDataSource"); - } else if ("log4jdbc".equals(database)) { - // jdbc:log4jdbc:${dbtype}:${db} - defaults.put("driverClassName", "net.sf.log4jdbc.DriverSpy"); + case "sqlserver" -> + // url => + // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] + defaults.put("dataSourceClassName", "com.microsoft.sqlserver.jdbc.SQLServerDataSource"); + case "oracle" -> + // url => jdbc:oracle:thin:@//:/ + defaults.put("dataSourceClassName", "oracle.jdbc.pool.OracleDataSource"); + case "pgsql" -> + // url => jdbc:pgsql://[:]/ + defaults.put("dataSourceClassName", "com.impossibl.postgres.jdbc.PGDataSource"); + case "postgresql", "cockroach", "yugabyte" -> + // url => jdbc:postgresql://host:port/database + defaults.put("dataSourceClassName", "org.postgresql.ds.PGSimpleDataSource"); + case "sybase" -> + // url => jdbc:jtds:sybase://[:][/] + defaults.put("dataSourceClassName", "com.sybase.jdbcx.SybDataSource"); + case "firebirdsql" -> + // jdbc:firebirdsql:host[/port]: + defaults.put("dataSourceClassName", "org.firebirdsql.pool.FBSimpleDataSource"); + case "sqlite" -> + // jdbc:sqlite:${db} + defaults.put("dataSourceClassName", "org.sqlite.SQLiteDataSource"); + // --- OLAP & Analytics --- + case "clickhouse" -> + // jdbc:clickhouse://:/ + defaults.put("dataSourceClassName", "com.clickhouse.jdbc.ClickHouseDataSource"); + case "snowflake" -> + // jdbc:snowflake://.snowflakecomputing.com/? + defaults.put("driverClassName", "net.snowflake.client.jdbc.SnowflakeDriver"); + case "redshift" -> + // jdbc:redshift://..redshift.amazonaws.com:/ + defaults.put("driverClassName", "com.amazon.redshift.Driver"); + case "trino" -> + // jdbc:trino://:// + defaults.put("driverClassName", "io.trino.jdbc.TrinoDriver"); + // --- Proxies & Wrappers --- + case "log4jdbc" -> + // jdbc:log4jdbc:${dbtype}:${db} + defaults.put("driverClassName", "net.sf.log4jdbc.DriverSpy"); + case "otel" -> + // jdbc:otel:${dbtype}:${db} + defaults.put( + "driverClassName", "io.opentelemetry.instrumentation.jdbc.OpenTelemetryDriver"); } return defaults; } + /** + * Forces the JVM to load and execute the static initialization block of the underlying JDBC + * Driver. This is specifically required for wrappers like OpenTelemetry that rely on + * java.sql.DriverManager instead of direct DataSource instantiation. + * + * @param database The target database type (e.g., "mysql", "postgresql") + * @param env The Jooby environment providing the classloader + */ + private static void forceLoadDriver(String database, Environment env) { + if (database == null) { + return; + } + + // Map the database string to its explicit java.sql.Driver implementation + var driverClassName = + switch (database) { + case "derby" -> "org.apache.derby.jdbc.ClientDriver"; + case "db2" -> "com.ibm.db2.jcc.DB2Driver"; + case "h2" -> "org.h2.Driver"; + case "hsqldb" -> "org.hsqldb.jdbc.JDBCDriver"; + case "mariadb" -> "org.mariadb.jdbc.Driver"; + case "mysql" -> "com.mysql.cj.jdbc.Driver"; // Modern 6.x/8.x Driver + case "sqlserver" -> "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + case "oracle" -> "oracle.jdbc.OracleDriver"; + case "pgsql" -> "com.impossibl.postgres.jdbc.PGDriver"; + case "postgresql", "cockroach", "yugabyte" -> "org.postgresql.Driver"; + case "sybase" -> "com.sybase.jdbc4.jdbc.SybDriver"; + case "firebirdsql" -> "org.firebirdsql.jdbc.FBDriver"; + case "sqlite" -> "org.sqlite.JDBC"; + // --- OLAP & Analytics --- + case "clickhouse" -> "com.clickhouse.jdbc.ClickHouseDriver"; + case "snowflake" -> "net.snowflake.client.jdbc.SnowflakeDriver"; + case "redshift" -> "com.amazon.redshift.Driver"; + case "trino" -> "io.trino.jdbc.TrinoDriver"; + default -> null; + }; + + if (driverClassName != null) { + try { + // The 'true' flag is the magic key: it forces the static {} block to execute, + // registering the driver globally with Java's DriverManager. + Class.forName(driverClassName, true, env.getClassLoader()); + } catch (ClassNotFoundException e) { + // Graceful fallback for legacy MySQL 5.x users if the modern driver is missing + if ("mysql".equals(database)) { + try { + Class.forName("com.mysql.jdbc.Driver", true, env.getClassLoader()); + } catch (ClassNotFoundException ignore) { + // Ignore missing driver; let the standard JDBC connection handle the failure later + } + } + } + } + } + static HikariConfig build(Environment env, String database) { Properties properties; Config config = env.getConfig(); @@ -379,7 +460,7 @@ static HikariConfig build(Environment env, String database) { dumpProperties(config, dbname, "dataSource.", properties::setProperty); } - /** *.dataSource AND *.hikari */ + /* *.dataSource AND *.hikari */ Stream.of(dbkey, dbname) .filter(Objects::nonNull) .distinct() @@ -403,7 +484,10 @@ static HikariConfig build(Environment env, String database) { configuration.remove("dataSource.url"); configuration.setProperty("jdbcUrl", dburl); } - + // wake driver for otel + if (dburl != null && dburl.startsWith("jdbc:otel:")) { + forceLoadDriver(databaseType(dburl.replace(":otel:", ":")), env); + } if (dbtype == null) { String poolName = Stream.of( diff --git a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java index 9dc392a3cd..6ea24cf36d 100644 --- a/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java +++ b/modules/jooby-jetty/src/main/java/io/jooby/jetty/JettyServer.java @@ -134,8 +134,6 @@ public io.jooby.Server start(@NonNull Jooby... application) { ((QueuedThreadPool) threadPool).setName("worker"); } - fireStart(List.of(application), threadPool); - var acceptors = 1; var selectors = options.getIoThreads(); server = new Server(threadPool); @@ -272,17 +270,21 @@ public io.jooby.Server start(@NonNull Jooby... application) { container.setIdleTimeout(Duration.ofMillis(timeout)); } server.setHandler(context); - server.start(); - // --- EXTRACT OS-ASSIGNED PORTS --- + for (var app : applications) { + var services = app.getServices(); + services.put(Server.class, server); + } + + fireStart(List.of(application), threadPool); + + server.start(); if (httpConector != null) { options.setPort(httpConector.getLocalPort()); } if (secureConnector != null) { options.setSecurePort(secureConnector.getLocalPort()); } - // --------------------------------- - fireReady(applications); } catch (Exception x) { if (io.jooby.Server.isAddressInUse(x.getCause())) { diff --git a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyEventLoopGroupImpl.java b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyEventLoopGroupImpl.java index cf8e4d8e71..a37600e62a 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyEventLoopGroupImpl.java +++ b/modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyEventLoopGroupImpl.java @@ -16,7 +16,7 @@ public class NettyEventLoopGroupImpl implements NettyEventLoopGroup { private final EventLoopGroup parent; private final EventLoopGroup child; private boolean closed; - private ExecutorService worker; + private final ExecutorService worker; public NettyEventLoopGroupImpl( NettyTransport transport, boolean single, int ioThreads, ExecutorService worker) { diff --git a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java index 20679d1c36..e649539abc 100644 --- a/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java +++ b/modules/jooby-netty/src/main/java/io/jooby/netty/NettyServer.java @@ -150,13 +150,18 @@ public Server start(@NonNull Jooby... application) { transport, singleEventLoopGroup, options.getIoThreads(), worker); } this.dateLoop = new NettyDateService(); - - fireStart(List.of(application), eventLoop.worker()); - var outputFactory = (NettyOutputFactory) getOutputFactory(); var allocator = outputFactory.getAllocator(); var http2 = options.isHttp2() == Boolean.TRUE; + for (var app : applications) { + var services = app.getServices(); + services.put(NettyEventLoopGroup.class, eventLoop); + services.put(ByteBufAllocator.class, allocator); + } + + fireStart(List.of(application), eventLoop.worker()); + // Retrieve the GrpcProcessor from the application's service registry GrpcProcessor grpcProcessor = http2 ? applications.get(0).getServices().getOrNull(GrpcProcessor.class) : null; diff --git a/modules/jooby-opentelemetry/pom.xml b/modules/jooby-opentelemetry/pom.xml new file mode 100644 index 0000000000..f15ee123f1 --- /dev/null +++ b/modules/jooby-opentelemetry/pom.xml @@ -0,0 +1,176 @@ + + + + 4.0.0 + + + io.jooby + modules + 4.3.1-SNAPSHOT + + jooby-opentelemetry + jooby-opentelemetry + + + + io.jooby + jooby + ${jooby.version} + + + + org.slf4j + jul-to-slf4j + + + + io.opentelemetry + opentelemetry-api + + + io.opentelemetry.instrumentation + opentelemetry-runtime-telemetry + + + io.opentelemetry + opentelemetry-sdk-extension-autoconfigure + + + + + com.zaxxer + HikariCP + true + + + io.opentelemetry.instrumentation + opentelemetry-hikaricp-3.0 + true + + + + + ch.qos.logback + logback-classic + true + + + io.opentelemetry.instrumentation + opentelemetry-logback-appender-1.0 + true + + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + true + + + io.opentelemetry.instrumentation + opentelemetry-log4j-appender-2.17 + true + + + + org.eclipse.jetty + jetty-server + true + + + + + io.netty + netty-common + true + + + io.jooby + jooby-netty + true + + + + + io.undertow + undertow-core + true + + + + + org.quartz-scheduler + quartz + true + + + io.opentelemetry.instrumentation + opentelemetry-quartz-2.0 + true + + + + + com.github.kagkarlsson + db-scheduler + ${db-scheduler.version} + true + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + org.mockito + mockito-core + test + + + + org.mockito + mockito-junit-jupiter + test + + + io.opentelemetry + opentelemetry-sdk-testing + test + + + org.assertj + assertj-core + + + + + + + io.opentelemetry + opentelemetry-bom + 1.60.1 + pom + import + + + io.opentelemetry.instrumentation + opentelemetry-instrumentation-bom-alpha + 2.26.1-alpha + pom + import + + + + diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelExtension.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelExtension.java new file mode 100644 index 0000000000..a586c3ba0b --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelExtension.java @@ -0,0 +1,36 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import io.jooby.Jooby; +import io.opentelemetry.api.OpenTelemetry; + +/** + * Extension point for OpenTelemetry integrations within a Jooby application. + * + *

While {@link OtelModule} is responsible for bootstrapping the core OpenTelemetry SDK, this + * interface allows developers to seamlessly attach secondary instrumentation modules (such as + * Logback appenders, HikariCP metrics, or Quartz job tracers) to the running SDK. + * + *

Lifecycle: Extensions are not executed immediately when passed to the {@code + * OtelModule} constructor. Instead, their execution is deferred until the Jooby application fires + * its {@code onStarting} event. This guarantees that the primary OpenTelemetry instance is fully + * configured and safely registered before any extensions attempt to use it. + */ +@FunctionalInterface +public interface OtelExtension { + + /** + * Installs and binds the OpenTelemetry extension to the application. + * + * @param application The current Jooby application. Extensions can use this to read application + * configuration, register internal services, or attach additional lifecycle hooks (e.g., + * closing resources during {@code onStop}). + * @param openTelemetry The fully constructed and configured OpenTelemetry instance. + * @throws Exception If the extension fails to initialize or attach its instrumentation. + */ + void install(Jooby application, OpenTelemetry openTelemetry) throws Exception; +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelHttpTracing.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelHttpTracing.java new file mode 100644 index 0000000000..8a57fa5d03 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelHttpTracing.java @@ -0,0 +1,127 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import static io.opentelemetry.context.Context.current; + +import io.jooby.Context; +import io.jooby.Route; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.propagation.TextMapGetter; + +/** + * OpenTelemetry HTTP tracing filter for Jooby routes. + * + *

This filter intercepts incoming HTTP requests and automatically creates an OpenTelemetry + * {@link SpanKind#SERVER} span for the request lifecycle. It acts as the primary entry point for + * distributed tracing in the web layer. + * + *

Features

+ * + *
    + *
  • Distributed Context Extraction: Automatically extracts W3C Trace Context + * headers (e.g., {@code traceparent}) from incoming requests to continue existing traces + * spanning multiple microservices. + *
  • Safe Span Naming: Uses the Jooby route pattern (e.g., {@code GET + * /api/users/{id}}) rather than the raw URI to prevent metric high-cardinality issues. + *
  • Semantic Conventions: Automatically populates standard HTTP attributes + * ({@code http.request.method}, {@code http.response.status_code}, etc.). + *
  • Asynchronous Safety: Ties the span closure to Jooby's {@code onComplete} + * hook, ensuring the span is accurately timed even if the route executes asynchronously. + *
+ * + *

Usage

+ * + *

Register this filter globally in your application using {@code use()} or {@code decorator()}. + * It must be registered after {@link OtelModule} is installed. + * + *

{@code
+ * {
+ * install(new OtelModule());
+ * use(new OtelHttpTracing());
+ * * get("/users/{id}", ctx -> "User " + ctx.path("id").value());
+ * }
+ * }
+ * + * @author edgar + * @since 4.3.1 + */ +public class OtelHttpTracing implements Route.Filter { + + /** + * Intercepts the HTTP request to initialize, populate, and eventually close the OpenTelemetry + * span. + * + * @param next The next handler in the routing chain. + * @return A wrapped route handler containing the tracing logic. + */ + @Override + public Route.Handler apply(Route.Handler next) { + return ctx -> { + // Create a high-cardinality-safe span name: e.g., "GET /api/users/{id}" + var spanName = ctx.getMethod() + " " + ctx.getRoute().getPattern(); + var tracer = ctx.require(Tracer.class); + var otel = ctx.require(OpenTelemetry.class); + var propagator = otel.getPropagators().getTextMapPropagator(); + + var extractedContext = propagator.extract(current(), ctx, JoobyRequestGetter.INSTANCE); + var span = + tracer + .spanBuilder(spanName) + .setParent(extractedContext) + .setSpanKind(SpanKind.SERVER) + .setAttribute("http.request.method", ctx.getMethod()) + .setAttribute("url.path", ctx.getRequestPath()) + .setAttribute("http.route", ctx.getRoute().getPattern()) + .startSpan(); + + // Ensure the span is ended ONLY when the HTTP response is fully complete + ctx.onComplete( + context -> { + int statusCode = context.getResponseCode().value(); + span.setAttribute("http.response.status_code", statusCode); + if (statusCode >= 500) { + // Mark as error based on standard semantic conventions + span.setStatus(io.opentelemetry.api.trace.StatusCode.ERROR); + } + span.end(); + }); + + // Activate the span in the current thread scope + try (var scope = span.makeCurrent()) { + ctx.setAttribute("otel-span", span); + + return next.apply(ctx); + } catch (Throwable t) { + span.recordException(t); + span.setAttribute("http.response.status_code", ctx.getRouter().errorCode(t).value()); + throw t; + } + }; + } + + /** + * A bridge implementation allowing OpenTelemetry to extract distributed tracing headers directly + * from a Jooby {@link Context}. + */ + enum JoobyRequestGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(io.jooby.Context ctx) { + // Allows OTel to iterate over all header names if needed + return ctx.headerMap().keySet(); + } + + @Override + public String get(io.jooby.Context ctx, String key) { + // Safely extract the header value, returning null if it doesn't exist + return ctx.header(key).valueOrNull(); + } + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelModule.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelModule.java new file mode 100644 index 0000000000..1b6f9436b6 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/OtelModule.java @@ -0,0 +1,231 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import static io.jooby.SneakyThrows.throwingConsumer; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.slf4j.bridge.SLF4JBridgeHandler; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Extension; +import io.jooby.Jooby; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.instrumentation.runtimetelemetry.RuntimeTelemetry; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import jakarta.inject.Provider; + +/** + * OpenTelemetry module for Jooby. + * + *

This module integrates OpenTelemetry into your Jooby application, providing the foundational + * engine for distributed tracing, metrics, and log correlation. It handles the lifecycle of the + * {@link OpenTelemetry} SDK and registers the SDK, the default {@link Tracer}, and the fluent + * {@link Trace} utility into the Jooby application services. + * + *

Important: Installation Order

+ * + *

Because this module bootstraps the core telemetry engine and registers the OpenTelemetry + * instance into the application services, it must be installed at the very + * beginning of your application setup. Installing it early ensures that all subsequent + * routes, filters, and extensions have immediate access to the tracer and metric instruments. + * + *

Usage

+ * + *

Install the module into your application, passing any specific OpenTelemetry extensions you + * want to enable. To automatically trace HTTP requests, you must also append the {@code + * OtelHttpTracing} filter to your routing pipeline: + * + *

{@code
+ * {
+ * // 1. Install the core engine FIRST
+ * install(new OtelModule(
+ * new OtelLogback(),       // Injects Trace IDs into application logs
+ * new OtelServerMetrics(), // Exports HTTP server metrics (e.g., Netty, Undertow, Jetty)
+ * new OtelHikari()         // Traces database connection pools
+ * ));
+ *
+ * // 2. Add the tracing filter to the routing pipeline
+ * use(new OtelHttpTracing());
+ *
+ * // 3. Define routes
+ * get("/books", ctx -> "List of books");
+ * }
+ * }
+ * + *

Route Tracing (OtelHttpTracing)

+ * + *

While {@code OtelModule} bootstraps the core OpenTelemetry engine, it does not automatically + * trace web requests. You must explicitly include {@code OtelHttpTracing}. + * + *

Note that {@code OtelHttpTracing} is not an {@link OtelExtension}; it is a + * native Jooby {@code Route.Filter}. It must be installed directly into the application's routing + * pipeline (e.g., via {@code use()}) to intercept, create, and propagate spans for incoming HTTP + * requests. + * + *

Manual Tracing

+ * + *

For tracing specific business logic, database queries, or external API calls, this module + * provides a fluent {@link Trace} utility. You can retrieve it from the route context or inject it + * directly into your service layer to safely create, configure, and execute custom spans without + * risking context leaks. + * + *

{@code
+ * get("/books/{isbn}", ctx -> {
+ *   Trace trace = ctx.require(Trace.class);
+ *   String isbn = ctx.path("isbn").value();
+ *   return trace.span("fetch_book")
+ *        .attribute("isbn", isbn)
+ *        .execute(span -> {
+ *           span.addEvent("Executing database query");
+ *           return repository.findByIsbn(isbn);
+ *         });
+ * });
+ * }
+ * + *

Configuration

+ * + *

The OpenTelemetry SDK is configured directly from your application's {@code application.conf}. + * Any property defined inside the {@code otel} block is automatically extracted and used to + * configure the underlying SDK components, such as exporters, protocols, and service attributes. + * + *

{@code
+ * otel {
+ * service.name = "jooby-api"
+ * traces.exporter = otlp
+ * metrics.exporter = otlp
+ * logs.exporter = otlp
+ * exporter.otlp.protocol = grpc
+ * exporter.otlp.endpoint = "http://localhost:4317"
+ * }
+ * }
+ * + *

If no {@code otel} configuration block is present in the application configuration, the module + * will fall back to a baseline, default SDK. + * + *

Extensions Lifecycle

+ * + *

Additional OpenTelemetry integrations (such as logging appenders or connection pool metrics) + * are provided via {@link OtelExtension} implementations. These extensions are not executed + * immediately upon module installation. + * + *

Instead, the module defers their execution by registering them to the application's {@code + * onStarting} lifecycle hook. This guarantees that the primary OpenTelemetry SDK is fully + * constructed, configured, and registered before any secondary extensions attempt to hook into it + * or emit telemetry data. + * + * @since 4.3.1 + * @author edgar + */ +public class OtelModule implements Extension { + + static { + SLF4JBridgeHandler.install(); + } + + private final OpenTelemetry openTelemetry; + private final List extensions; + + /** + * Creates a new OpenTelemetry module with a pre-configured OpenTelemetry instance. + * + * @param openTelemetry A pre-configured OpenTelemetry instance. + * @param extensions Optional extensions (e.g., OtelLogback, OtelHikari). + */ + public OtelModule(OpenTelemetry openTelemetry, OtelExtension... extensions) { + this.openTelemetry = openTelemetry; + this.extensions = List.of(extensions); + } + + /** + * Creates a new OpenTelemetry module. The SDK will be automatically configured based on the + * application's {@code application.conf}. + * + * @param extensions Optional extensions (e.g., OtelLogback, OtelHikari). + */ + public OtelModule(OtelExtension... extensions) { + this(null, extensions); + } + + @Override + public void install(@NonNull Jooby application) { + var otel = getOrCreate(application); + if (!isRunningInJoobyRun() && otel instanceof AutoCloseable closeableOtel) { + // Close the OpenTelemetry instance when the application is stopped, and we are not running + // in joobyRun. + application.onStop(closeableOtel); + } + var tracer = otel.getTracer("io.jooby.opentelemetry"); + + application.onStop(RuntimeTelemetry.create(otel)); + var services = application.getServices(); + services.put(OpenTelemetry.class, otel); + services.put(Tracer.class, tracer); + services.put(Trace.class, trace(tracer)); + + application.onStarting( + () -> extensions.forEach(throwingConsumer(ext -> ext.install(application, otel)))); + } + + private static Provider trace(Tracer tracer) { + return () -> new Trace(tracer); + } + + private boolean isRunningInJoobyRun() { + return getClass() + .getClassLoader() + .getClass() + .getName() + .equals("org.jboss.modules.ModuleClassLoader"); + } + + private OpenTelemetry getOrCreate(@NonNull Jooby application) { + if (this.openTelemetry == null) { + var appConfig = application.getConfig(); + Map otelProperties = new HashMap<>(); + if (appConfig.hasPath("otel")) { + var otelConfig = appConfig.getConfig("otel"); + otelConfig + .entrySet() + .forEach( + entry -> { + String key = "otel." + entry.getKey(); + String value = entry.getValue().unwrapped().toString(); + otelProperties.put(key, value); + }); + return safeCreateOnJoobyRun( + () -> + AutoConfiguredOpenTelemetrySdk.builder() + .addPropertiesSupplier(() -> otelProperties) + .disableShutdownHook() + .setResultAsGlobal() + .build() + .getOpenTelemetrySdk()); + } else { + return safeCreateOnJoobyRun(() -> OpenTelemetrySdk.builder().buildAndRegisterGlobal()); + } + } + return this.openTelemetry; + } + + private OpenTelemetry safeCreateOnJoobyRun(Supplier supplier) { + try { + return supplier.get(); + } catch (IllegalStateException ex) { + if (isRunningInJoobyRun()) { + return GlobalOpenTelemetry.get(); + } + throw ex; + } + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/Trace.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/Trace.java new file mode 100644 index 0000000000..ec0238fe94 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/Trace.java @@ -0,0 +1,128 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import java.util.function.Consumer; + +import io.jooby.SneakyThrows; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; + +/** + * Injectable utility for creating safe OpenTelemetry traces and spans. + * + * @author edgar + * @since 4.3.1 + */ +public class Trace { + + private final Tracer tracer; + + public Trace(Tracer tracer) { + this.tracer = tracer; + } + + /** + * Begins building a new OpenTelemetry span operation. + * + * @param name The name of the operation. + * @return A fluent builder to add attributes and execute logic. + */ + public Operation span(String name) { + return new Operation(tracer, name); + } + + public interface SpanTask { + T execute(Span span) throws Exception; + } + + public interface SpanRunnable { + void run(Span span) throws Exception; + } + + /** Represents an in-flight trace operation. */ + public static class Operation { + private final io.opentelemetry.api.trace.SpanBuilder otelSpanBuilder; + + private Operation(Tracer tracer, String name) { + this.otelSpanBuilder = tracer.spanBuilder(name); + } + + /** Escape hatch: Provides direct access to the native OpenTelemetry SpanBuilder. */ + public Operation configure(Consumer customizer) { + customizer.accept(otelSpanBuilder); + return this; + } + + public Operation attribute(String key, String value) { + otelSpanBuilder.setAttribute(key, value); + return this; + } + + public Operation attribute(String key, long value) { + otelSpanBuilder.setAttribute(key, value); + return this; + } + + public Operation attribute(String key, double value) { + otelSpanBuilder.setAttribute(key, value); + return this; + } + + public Operation attribute(String key, boolean value) { + otelSpanBuilder.setAttribute(key, value); + return this; + } + + /** Supports strongly-typed OpenTelemetry semantic convention keys. */ + public Operation attribute(AttributeKey key, T value) { + otelSpanBuilder.setAttribute(key, value); + return this; + } + + public Operation kind(io.opentelemetry.api.trace.SpanKind kind) { + otelSpanBuilder.setSpanKind(kind); + return this; + } + + public Operation rootContext() { + otelSpanBuilder.setNoParent(); + return this; + } + + /** Executes logic that returns a value within the span context. */ + public T execute(SpanTask block) { + var span = otelSpanBuilder.startSpan(); + try (var scope = span.makeCurrent()) { + return block.execute(span); + } catch (Throwable t) { + span.recordException(t); + span.setStatus( + StatusCode.ERROR, t.getMessage() != null ? t.getMessage() : t.getClass().getName()); + throw SneakyThrows.propagate(t); + } finally { + span.end(); + } + } + + /** Executes void logic within the span context. */ + public void run(SpanRunnable block) { + var span = otelSpanBuilder.startSpan(); + try (var scope = span.makeCurrent()) { + block.run(span); + } catch (Throwable t) { + span.recordException(t); + span.setStatus( + StatusCode.ERROR, t.getMessage() != null ? t.getMessage() : t.getClass().getName()); + throw SneakyThrows.propagate(t); + } finally { + span.end(); + } + } + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelDbScheduler.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelDbScheduler.java new file mode 100644 index 0000000000..d806dd88e9 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelDbScheduler.java @@ -0,0 +1,154 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import com.github.kagkarlsson.scheduler.event.ExecutionChain; +import com.github.kagkarlsson.scheduler.event.ExecutionInterceptor; +import com.github.kagkarlsson.scheduler.task.CompletionHandler; +import com.github.kagkarlsson.scheduler.task.ExecutionContext; +import com.github.kagkarlsson.scheduler.task.TaskInstance; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; + +/** + * OpenTelemetry instrumentation for the {@code db-scheduler} library. + * + *

This class implements {@link ExecutionInterceptor} to automatically generate traces and + * metrics for every scheduled task execution. + * + *

Traces

+ * + *

Creates an {@link SpanKind#INTERNAL} span named {@code Job } for each execution. The + * span includes the following attributes: + * + *

    + *
  • {@code job.system}: Always set to {@code "db-scheduler"} + *
  • {@code job.id}: The unique identifier of the task instance + *
+ * + * Exceptions thrown during execution are recorded on the span, and the span status is set to {@link + * StatusCode#ERROR}. + * + *

Metrics

+ * + *

Records the following metrics under the {@code io.jooby.db-scheduler} meter: + * + *

    + *
  • {@code dbscheduler.task.completions} (Counter): Tracks total task executions. + *
  • {@code dbscheduler.task.duration} (Histogram): Tracks execution time in seconds. + *
+ * + * Both metrics include the {@code task} name and the execution {@code result} (either {@code "ok"} + * or {@code "failed"}) as attributes. + * + *

Usage

+ * + *
{@code
+ * install(new OtelModule(...));
+ *
+ * install(new DbSchedulerModule()
+ *    .withExecutionInterceptor(new OtelDbScheduler(require(OpenTelemetry.class)))
+ * )
+ * }
+ * + * @author edgar + * @since 4.3.1 + */ +public class OtelDbScheduler implements ExecutionInterceptor { + private final Tracer tracer; + private final LongCounter completionsCounter; + private final DoubleHistogram durationHistogram; + + /** + * Creates a new OpenTelemetry interceptor for db-scheduler. + * + * @param openTelemetry The fully configured OpenTelemetry instance used to extract the {@link + * Tracer} and {@link io.opentelemetry.api.metrics.Meter}. + */ + public OtelDbScheduler(OpenTelemetry openTelemetry) { + this.tracer = openTelemetry.getTracer("io.jooby.db-scheduler"); + var meter = openTelemetry.getMeter("io.jooby.db-scheduler"); + + this.completionsCounter = + meter + .counterBuilder("dbscheduler.task.completions") + .setDescription("Successes and failures by task") + .setUnit("{completion}") + .build(); + + this.durationHistogram = + meter + .histogramBuilder("dbscheduler.task.duration") + .setDescription("Duration of executions") + .setUnit("s") + .build(); + } + + /** + * Intercepts the task execution to start a span, measure duration, and record metrics. + * + * @param taskInstance The instance of the task being executed. + * @param executionContext The current execution context. + * @param chain The execution chain to proceed. + * @return The completion handler returned by the underlying task or chain. + */ + @Override + public CompletionHandler execute( + TaskInstance taskInstance, ExecutionContext executionContext, ExecutionChain chain) { + + var taskName = taskInstance.getTaskName(); + var startTime = System.nanoTime(); + + var span = + tracer + .spanBuilder("Job " + taskName) + .setSpanKind(SpanKind.INTERNAL) + .setAttribute("job.system", "db-scheduler") + .setAttribute("job.id", taskInstance.getId()) + .startSpan(); + + try (var scope = span.makeCurrent()) { + var result = chain.proceed(taskInstance, executionContext); + + recordMetrics(taskName, startTime, "ok"); + return result; + + } catch (Throwable t) { + span.recordException(t); + span.setStatus(StatusCode.ERROR); + + recordMetrics(taskName, startTime, "failed"); + throw t; + } finally { + span.end(); + } + } + + /** + * Records the completion and duration metrics for the task execution. + * + * @param taskName The name of the executed task. + * @param startTimeNanos The start time of the execution in nanoseconds. + * @param result The outcome of the execution (e.g., "ok" or "failed"). + */ + private void recordMetrics(String taskName, long startTimeNanos, String result) { + var durationSeconds = (System.nanoTime() - startTimeNanos) / 1_000_000_000.0; + + var attributes = + Attributes.of( + AttributeKey.stringKey("task"), taskName, + AttributeKey.stringKey("result"), result); + + completionsCounter.add(1, attributes); + durationHistogram.record(durationSeconds, attributes); + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelHikari.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelHikari.java new file mode 100644 index 0000000000..3a53bc94b5 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelHikari.java @@ -0,0 +1,70 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import com.zaxxer.hikari.HikariDataSource; +import io.jooby.Jooby; +import io.jooby.Reified; +import io.jooby.opentelemetry.OtelExtension; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.hikaricp.v3_0.HikariTelemetry; + +/** + * OpenTelemetry extension for HikariCP connection pools. + * + *

This extension automatically instruments all {@link HikariDataSource} instances registered + * within the Jooby application, exporting critical connection pool metrics (such as active + * connections, idle connections, and connection timeouts) to the OpenTelemetry backend. + * + *

Required Dependency

+ * + *

To use this extension, you must add the official OpenTelemetry HikariCP instrumentation + * library to your project's classpath: + * + *

{@code
+ * 
+ * io.opentelemetry.instrumentation
+ * opentelemetry-hikaricp-3.0
+ * 
+ * }
+ * + *

Installation Order

+ * + *

Application installation order is critical. The {@code OtelModule} must be installed + * first, followed by the {@code HikariModule}. + * + *

{@code
+ * {
+ * // 1. Install OpenTelemetry with the Hikari extension FIRST
+ * install(new OtelModule(new OtelHikari()));
+ *
+ * // 2. Install HikariModule NEXT
+ * install(new HikariModule());
+ * }
+ * }
+ * + *

Lifecycle Note: Although {@code OtelModule} is installed first, this extension defers + * its execution to the application's {@code onStarting} lifecycle hook. This ensures that all data + * sources configured by the subsequent {@code HikariModule} are fully initialized and available in + * the service registry before the metrics tracker is applied. + * + * @since 4.3.1 + * @author edgar + */ +public class OtelHikari implements OtelExtension { + + @Override + public void install(Jooby application, OpenTelemetry openTelemetry) { + java.util.List dataSources = + application.require(Reified.list(HikariDataSource.class)); + var hikariTelemetry = HikariTelemetry.create(openTelemetry); + + // Apply the telemetry metrics tracker to every configured Hikari connection pool + for (HikariDataSource dataSource : dataSources) { + dataSource.setMetricsTrackerFactory(hikariTelemetry.createMetricsTrackerFactory()); + } + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2.java new file mode 100644 index 0000000000..d45226e50d --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2.java @@ -0,0 +1,86 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; + +import io.jooby.Jooby; +import io.jooby.opentelemetry.OtelExtension; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender; + +/** + * OpenTelemetry extension for Log4j2. + * + *

This extension automatically instruments the Log4j2 logging framework by dynamically attaching + * an {@link OpenTelemetryAppender} to the root logger. This ensures that all application logs are + * seamlessly exported to your OpenTelemetry backend, automatically correlated with active trace and + * span IDs. + * + *

Required Dependency

+ * + *

To use this extension, you must add the official OpenTelemetry Log4j2 appender instrumentation + * library to your project's classpath: + * + *

{@code
+ * 
+ * io.opentelemetry.instrumentation
+ * opentelemetry-log4j-appender-2.17
+ * 
+ * }
+ * + *

Usage

+ * + *

Register this extension inside the core {@code OtelModule} during application setup: + * + *

{@code
+ * {
+ * install(new OtelModule(
+ * new OtelLog4j2()
+ * ));
+ * }
+ * }
+ * + *

Runtime Requirements

+ * + *

This extension requires {@code log4j-core} to be present at runtime to function correctly. It + * accesses the underlying {@link LoggerContext} to dynamically inject the appender. If the + * application is routing logs through a different backend (e.g., Logback or SimpleLogger), this + * extension will gracefully fail and log a warning without crashing the application. + * + * @since 4.3.1 + * @author edgar + */ +public class OtelLog4j2 implements OtelExtension { + + @Override + public void install(Jooby application, OpenTelemetry openTelemetry) { + var currentContext = LogManager.getContext(application.getClassLoader(), false); + + if (currentContext instanceof LoggerContext loggerContext) { + var config = loggerContext.getConfiguration(); + + var otelAppender = + OpenTelemetryAppender.builder() + .setName("OpenTelemetry") + .setOpenTelemetry(openTelemetry) + .build(); + + otelAppender.start(); + config.addAppender(otelAppender); + + config.getRootLogger().addAppender(otelAppender, null, null); + loggerContext.updateLoggers(); + } else { + application + .getLog() + .warn( + "Log4j2OpenTelemetry requires log4j-core. Current context is: {}", + currentContext.getClass().getName()); + } + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLogback.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLogback.java new file mode 100644 index 0000000000..69b2abf183 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelLogback.java @@ -0,0 +1,86 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import io.jooby.Jooby; +import io.jooby.opentelemetry.OtelExtension; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; + +/** + * OpenTelemetry extension for Logback. + * + *

This extension automatically instruments the Logback logging framework by dynamically + * attaching an {@link OpenTelemetryAppender} to the root logger. This ensures that all application + * logs are seamlessly exported to your OpenTelemetry backend, automatically correlated with active + * trace and span IDs. + * + *

Required Dependency

+ * + *

To use this extension, you must add the official OpenTelemetry Logback appender + * instrumentation library to your project's classpath: + * + *

{@code
+ * 
+ * io.opentelemetry.instrumentation
+ * opentelemetry-logback-appender-1.0
+ * 
+ * }
+ * + *

Usage

+ * + *

Register this extension inside the core {@code OtelModule} during application setup: + * + *

{@code
+ * {
+ * install(new OtelModule(
+ * new OtelLogback()
+ * ));
+ * }
+ * }
+ * + *

Runtime Requirements

+ * + *

This extension requires Logback to be the active SLF4J binding at runtime. It verifies that + * the underlying factory is a {@link LoggerContext} before injecting the appender. If the + * application routes logs through a different backend (e.g., SimpleLogger, Log4j2), this extension + * will safely bypass installation and log a warning. + * + * @since 4.3.1 + * @author edgar + */ +public class OtelLogback implements OtelExtension { + + @Override + public void install(Jooby application, OpenTelemetry openTelemetry) { + var loggerFactory = LoggerFactory.getILoggerFactory(); + + // Ensure we are actually running Logback before casting + if (loggerFactory instanceof LoggerContext loggerContext) { + var otelAppender = new OpenTelemetryAppender(); + otelAppender.setName("OpenTelemetry"); + otelAppender.setContext(loggerContext); + otelAppender.setOpenTelemetry(openTelemetry); + + // Start the appender + otelAppender.start(); + + // Attach it to the Root Logger so it catches everything + var rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.addAppender(otelAppender); + } else { + application + .getLog() + .warn( + "LogbackOpenTelemetry requires Logback. Current factory: {}", + loggerFactory.getClass().getName()); + } + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelQuartz.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelQuartz.java new file mode 100644 index 0000000000..761a3f2158 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelQuartz.java @@ -0,0 +1,68 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import org.quartz.Scheduler; + +import io.jooby.Jooby; +import io.jooby.opentelemetry.OtelExtension; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.quartz.v2_0.QuartzTelemetry; + +/** + * OpenTelemetry extension for the Quartz scheduler. + * + *

This extension automatically instruments the Quartz {@link Scheduler} registered within the + * Jooby application. It tracks the execution of all Quartz jobs, creating individual spans for each + * execution to monitor scheduling delays, execution durations, and potential failures. + * + *

Required Dependency

+ * + *

To use this extension, you must add the official OpenTelemetry Quartz instrumentation library + * to your project's classpath: + * + *

{@code
+ * 
+ * io.opentelemetry.instrumentation
+ * opentelemetry-quartz-2.0
+ * 
+ * }
+ * + *

Installation Order

+ * + *

The {@code OtelModule} should be installed alongside the Jooby {@code QuartzModule}. + * + *

{@code
+ * {
+ * // 1. Install OpenTelemetry with the Quartz extension FIRST
+ * install(new OtelModule(new OtelQuartz()));
+ *
+ * // 2. Install QuartzModule NEXT
+ * install(new QuartzModule(MyJobs.class));
+ * }
+ * }
+ * + *

Lifecycle Note: Although {@code OtelModule} is installed first, this extension defers + * its execution to the application's {@code onStarting} lifecycle hook. This ensures that the + * {@link Scheduler} configured by the {@code QuartzModule} is fully initialized and available in + * the service registry before the OpenTelemetry listener is attached to it. + * + * @since 4.3.1 + * @author edgar + */ +public class OtelQuartz implements OtelExtension { + + @Override + public void install(Jooby application, OpenTelemetry openTelemetry) throws Exception { + var scheduler = application.require(Scheduler.class); + + // Build the official OTel listener + var quartzTelemetry = QuartzTelemetry.builder(openTelemetry).build(); + quartzTelemetry.configure(scheduler); + + application.getLog().debug("OpenTelemetry Quartz JobListener installed."); + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelServerMetrics.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelServerMetrics.java new file mode 100644 index 0000000000..1afd8653c8 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/instrumentation/OtelServerMetrics.java @@ -0,0 +1,296 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.QueuedThreadPool; + +import io.jooby.Jooby; +import io.jooby.Server; +import io.jooby.netty.NettyEventLoopGroup; +import io.jooby.opentelemetry.OtelExtension; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.Meter; + +/** + * OpenTelemetry extension for Jooby HTTP servers. + * + *

This extension automatically detects the underlying HTTP server running your Jooby application + * (Jetty, Netty, or Undertow) and exports native, server-specific operational metrics to your + * OpenTelemetry backend under the {@code io.jooby.server} meter. + * + *

Supported Servers & Metrics

+ * + *

Netty/Vert.x

+ * + *
    + *
  • {@code server.netty.eventloop.pending_tasks} / {@code count}: Tracks IO event loop threads + * and pending tasks. High pending tasks often indicate blocking code on the event loop. + *
  • {@code server.netty.acceptor.count}: Tracks dedicated TCP acceptor threads. + *
  • {@code server.netty.worker.*}: Tracks active threads, queue sizes, and pending tasks in the + * worker executor. + *
  • {@code server.netty.memory.direct_used} / {@code heap_used}: Tracks ByteBufAllocator memory + * consumption. + *
+ * + *

Jetty

+ * + *
    + *
  • {@code server.jetty.threads.active} / {@code idle}: Tracks the state of the underlying + * {@link QueuedThreadPool}. + *
  • {@code server.jetty.queue.size}: Tracks jobs queued waiting for an available Jetty thread. + *
  • {@code server.jetty.connections.active}: Tracks active TCP connections across all server + * connectors. + *
+ * + *

Undertow

+ * + *
    + *
  • {@code server.undertow.worker.threads.active} / {@code queue.size}: Tracks the XNIO worker + * pool capacity and backlog. + *
  • {@code server.undertow.eventloop.count}: Tracks active IO (Event Loop) threads managed by + * XNIO. + *
  • {@code server.undertow.connections.active}: Tracks active connections across all Undertow + * listeners. + *
+ * + *

Usage

+ * + *

Register this extension inside the core {@code OtelModule} during application setup: + * + *

{@code
+ * {
+ * install(new OtelModule(
+ * new OtelServerMetrics()
+ * ));
+ * }
+ * }
+ * + * @since 4.3.1 + * @author edgar + */ +public class OtelServerMetrics implements OtelExtension { + + @Override + public void install(Jooby application, OpenTelemetry openTelemetry) { + + var server = application.require(Server.class); + var meter = openTelemetry.getMeter("io.jooby.server"); + + // Route the instrumentation based on the active server + switch (server.getName().toLowerCase()) { + case "jetty": + instrumentJetty(application, meter); + break; + case "netty", "vertx": + instrumentNetty(application, meter); + break; + case "undertow": + instrumentUndertow(application, meter); + break; + default: + application + .getLog() + .debug("No specific OTel metrics mapped for server: {}", server.getName()); + } + } + + private void instrumentJetty(Jooby application, Meter meter) { + var jettyServer = application.require(org.eclipse.jetty.server.Server.class); + + if (jettyServer.getThreadPool() instanceof QueuedThreadPool threadPool) { + meter + .gaugeBuilder("server.jetty.threads.active") + .setDescription("Number of active (busy) threads in Jetty pool") + .setUnit("{thread}") + .buildWithCallback(m -> m.record(threadPool.getBusyThreads())); + + meter + .gaugeBuilder("server.jetty.threads.idle") + .setDescription("Number of idle threads in Jetty pool") + .setUnit("{thread}") + .buildWithCallback(m -> m.record(threadPool.getIdleThreads())); + + meter + .gaugeBuilder("server.jetty.queue.size") + .setDescription("Number of jobs queued waiting for a Jetty thread") + .setUnit("{job}") + .buildWithCallback(m -> m.record(threadPool.getQueueSize())); + } + + meter + .gaugeBuilder("server.jetty.connections.active") + .setDescription("Number of active TCP connections to Jetty") + .setUnit("{connection}") + .buildWithCallback( + m -> { + long totalConnections = 0; + for (var connector : jettyServer.getConnectors()) { + if (connector instanceof ServerConnector serverConnector) { + totalConnections += serverConnector.getConnectedEndPoints().size(); + } + } + m.record(totalConnections); + }); + } + + private void instrumentNetty(Jooby application, Meter meter) { + var nettyGroups = application.require(NettyEventLoopGroup.class); + // --- 1. EVENT LOOP (IO / CHILD) METRICS --- + meter + .gaugeBuilder("server.netty.eventloop.pending_tasks") + .setDescription( + "Number of pending tasks in Netty IO event loops. High numbers indicate blocking code.") + .setUnit("{task}") + .buildWithCallback( + m -> { + long totalPending = 0; + for (var eventExecutor : nettyGroups.eventLoop()) { + if (eventExecutor + instanceof io.netty.util.concurrent.SingleThreadEventExecutor stee) { + totalPending += stee.pendingTasks(); + } + } + m.record(totalPending); + }); + + meter + .gaugeBuilder("server.netty.eventloop.count") + .setDescription("Number of active Netty IO event loop threads") + .setUnit("{thread}") + .buildWithCallback( + m -> { + long count = 0; + for (var ignored : nettyGroups.eventLoop()) { + count++; + } + m.record(count); + }); + + // --- 2. ACCEPTOR METRICS --- + // Safely verify the acceptor exists AND is a distinct pool from the EventLoop + if (nettyGroups.acceptor() != nettyGroups.eventLoop()) { + meter + .gaugeBuilder("server.netty.acceptor.count") + .setDescription("Number of active acceptor threads handling TCP connections") + .setUnit("{thread}") + .buildWithCallback( + m -> { + long count = 0; + for (var ignored : nettyGroups.acceptor()) count++; + m.record(count); + }); + } + + // --- 3. WORKER EXECUTOR METRICS --- + var worker = nettyGroups.worker(); + + if (worker instanceof java.util.concurrent.ThreadPoolExecutor threadPool) { + meter + .gaugeBuilder("server.netty.worker.threads.active") + .setDescription("Number of active threads in the Java worker pool") + .setUnit("{thread}") + .buildWithCallback(m -> m.record(threadPool.getActiveCount())); + + meter + .gaugeBuilder("server.netty.worker.queue.size") + .setDescription("Number of tasks queued waiting for a Java worker thread") + .setUnit("{task}") + .buildWithCallback(m -> m.record(threadPool.getQueue().size())); + + // Scenario B: Worker is a native Netty DefaultEventExecutorGroup + } else if (worker instanceof io.netty.util.concurrent.EventExecutorGroup nettyExecutor) { + meter + .gaugeBuilder("server.netty.worker.pending_tasks") + .setDescription("Number of pending tasks in the Netty EventExecutorGroup") + .setUnit("{task}") + .buildWithCallback( + m -> { + long totalPending = 0; + for (var executor : nettyExecutor) { + if (executor instanceof io.netty.util.concurrent.SingleThreadEventExecutor stee) { + totalPending += stee.pendingTasks(); + } + } + m.record(totalPending); + }); + + meter + .gaugeBuilder("server.netty.worker.threads.count") + .setDescription("Number of active Netty worker threads") + .setUnit("{thread}") + .buildWithCallback( + m -> { + long count = 0; + for (var ignored : nettyExecutor) count++; + m.record(count); + }); + } + + // --- 4. GLOBAL MEMORY METRICS --- + var allocator = application.require(io.netty.buffer.ByteBufAllocator.class); + + if (allocator instanceof io.netty.buffer.ByteBufAllocatorMetricProvider metricProvider) { + var metric = metricProvider.metric(); + meter + .gaugeBuilder("server.netty.memory.direct_used") + .setDescription("Used direct memory by Netty ByteBufAllocator") + .setUnit("By") + .buildWithCallback(m -> m.record(metric.usedDirectMemory())); + + meter + .gaugeBuilder("server.netty.memory.heap_used") + .setDescription("Used heap memory by Netty ByteBufAllocator") + .setUnit("By") + .buildWithCallback(m -> m.record(metric.usedHeapMemory())); + } + } + + private void instrumentUndertow(Jooby application, Meter meter) { + var undertow = application.require(io.undertow.Undertow.class); + var worker = undertow.getWorker(); + + // Extract the public management bean to read the thread states safely + var mxBean = worker.getMXBean(); + + // 1. Worker Pool Metrics + meter + .gaugeBuilder("server.undertow.worker.threads.active") + .setDescription("Number of active task threads in the XNIO worker pool") + .setUnit("{thread}") + .buildWithCallback(m -> m.record(mxBean.getBusyWorkerThreadCount())); + + meter + .gaugeBuilder("server.undertow.worker.queue.size") + .setDescription("Number of tasks queued in the XNIO worker") + .setUnit("{task}") + .buildWithCallback(m -> m.record(mxBean.getWorkerQueueSize())); + + // 2. Event Loop (IO Thread) Count + meter + .gaugeBuilder("server.undertow.eventloop.count") + .setDescription("Number of active IO (Event Loop) threads managed by XNIO") + .setUnit("{thread}") + .buildWithCallback(m -> m.record(mxBean.getIoThreadCount())); + + // 3. Event Loop Load (Via Connector Statistics) + meter + .gaugeBuilder("server.undertow.connections.active") + .setDescription("Active connections being managed by the Undertow event loops") + .setUnit("{connection}") + .buildWithCallback( + m -> { + long activeConnections = 0; + for (var listener : undertow.getListenerInfo()) { + var stats = listener.getConnectorStatistics(); + if (stats != null) { + activeConnections += stats.getActiveConnections(); + } + } + m.record(activeConnections); + }); + } +} diff --git a/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/package-info.java b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/package-info.java new file mode 100644 index 0000000000..6b67e54c73 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/io/jooby/opentelemetry/package-info.java @@ -0,0 +1,105 @@ +/** + * OpenTelemetry module for Jooby. + * + *

This module integrates OpenTelemetry into your Jooby application, providing the foundational + * engine for distributed tracing, metrics, and log correlation. It handles the lifecycle of the + * {@link io.opentelemetry.api.OpenTelemetry} SDK and registers the SDK, the default {@link + * io.opentelemetry.api.trace.Tracer}, and the fluent {@link io.jooby.opentelemetry.Trace} utility + * into the Jooby application services. + * + *

Important: Installation Order

+ * + *

Because this module bootstraps the core telemetry engine and registers the OpenTelemetry + * instance into the application services, it must be installed at the very + * beginning of your application setup. Installing it early ensures that all subsequent + * routes, filters, and extensions have immediate access to the tracer and metric instruments. + * + *

Usage

+ * + *

Install the module into your application, passing any specific OpenTelemetry extensions you + * want to enable. To automatically trace HTTP requests, you must also append the {@code + * OtelHttpTracing} filter to your routing pipeline: + * + *

{@code
+ * {
+ * // 1. Install the core engine FIRST
+ * install(new OtelModule(
+ * new OtelLogback(),       // Injects Trace IDs into application logs
+ * new OtelServerMetrics(), // Exports HTTP server metrics (e.g., Netty, Undertow, Jetty)
+ * new OtelHikari()         // Traces database connection pools
+ * ));
+ *
+ * // 2. Add the tracing filter to the routing pipeline
+ * use(new OtelHttpTracing());
+ *
+ * // 3. Define routes
+ * get("/books", ctx -> "List of books");
+ * }
+ * }
+ * + *

Route Tracing (OtelHttpTracing)

+ * + *

While {@code OtelModule} bootstraps the core OpenTelemetry engine, it does not automatically + * trace web requests. You must explicitly include {@code OtelHttpTracing}. + * + *

Note that {@code OtelHttpTracing} is not an {@link + * io.jooby.opentelemetry.OtelExtension}; it is a native Jooby {@code Route.Filter}. It must be + * installed directly into the application's routing pipeline (e.g., via {@code use()}) to + * intercept, create, and propagate spans for incoming HTTP requests. + * + *

Manual Tracing

+ * + *

For tracing specific business logic, database queries, or external API calls, this module + * provides a fluent {@link io.jooby.opentelemetry.Trace} utility. You can retrieve it from the + * route context or inject it directly into your service layer to safely create, configure, and + * execute custom spans without risking context leaks. + * + *

{@code
+ * get("/books/{isbn}", ctx -> {
+ *   Trace trace = ctx.require(Trace.class);
+ *   String isbn = ctx.path("isbn").value();
+ *   return trace.span("fetch_book")
+ *        .attribute("isbn", isbn)
+ *        .execute(span -> {
+ *           span.addEvent("Executing database query");
+ *           return repository.findByIsbn(isbn);
+ *         });
+ * });
+ * }
+ * + *

Configuration

+ * + *

The OpenTelemetry SDK is configured directly from your application's {@code application.conf}. + * Any property defined inside the {@code otel} block is automatically extracted and used to + * configure the underlying SDK components, such as exporters, protocols, and service attributes. + * + *

{@code
+ * otel {
+ * service.name = "jooby-api"
+ * traces.exporter = otlp
+ * metrics.exporter = otlp
+ * logs.exporter = otlp
+ * exporter.otlp.protocol = grpc
+ * exporter.otlp.endpoint = "http://localhost:4317"
+ * }
+ * }
+ * + *

If no {@code otel} configuration block is present in the application configuration, the module + * will fall back to a baseline, default SDK. + * + *

Extensions Lifecycle

+ * + *

Additional OpenTelemetry integrations (such as logging appenders or connection pool metrics) + * are provided via {@link io.jooby.opentelemetry.OtelExtension} implementations. These extensions + * are not executed immediately upon module installation. + * + *

Instead, the module defers their execution by registering them to the application's {@code + * onStarting} lifecycle hook. This guarantees that the primary OpenTelemetry SDK is fully + * constructed, configured, and registered before any secondary extensions attempt to hook into it + * or emit telemetry data. + * + * @since 4.3.1 + * @author edgar + */ +@edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault +package io.jooby.opentelemetry; diff --git a/modules/jooby-opentelemetry/src/main/java/module-info.java b/modules/jooby-opentelemetry/src/main/java/module-info.java new file mode 100644 index 0000000000..75c94b0b06 --- /dev/null +++ b/modules/jooby-opentelemetry/src/main/java/module-info.java @@ -0,0 +1,152 @@ +/** + * OpenTelemetry module for Jooby. + * + *

This module integrates OpenTelemetry into your Jooby application, providing the foundational + * engine for distributed tracing, metrics, and log correlation. It handles the lifecycle of the + * {@link io.opentelemetry.api.OpenTelemetry} SDK and registers the SDK, the default {@link + * io.opentelemetry.api.trace.Tracer}, and the fluent {@link io.jooby.opentelemetry.Trace} utility + * into the Jooby application services. + * + *

Important: Installation Order

+ * + *

Because this module bootstraps the core telemetry engine and registers the OpenTelemetry + * instance into the application services, it must be installed at the very + * beginning of your application setup. Installing it early ensures that all subsequent + * routes, filters, and extensions have immediate access to the tracer and metric instruments. + * + *

Usage

+ * + *

Install the module into your application, passing any specific OpenTelemetry extensions you + * want to enable. To automatically trace HTTP requests, you must also append the {@code + * OtelHttpTracing} filter to your routing pipeline: + * + *

{@code
+ * {
+ * // 1. Install the core engine FIRST
+ * install(new OtelModule(
+ * new OtelLogback(),       // Injects Trace IDs into application logs
+ * new OtelServerMetrics(), // Exports HTTP server metrics (e.g., Netty, Undertow, Jetty)
+ * new OtelHikari()         // Traces database connection pools
+ * ));
+ *
+ * // 2. Add the tracing filter to the routing pipeline
+ * use(new OtelHttpTracing());
+ *
+ * // 3. Define routes
+ * get("/books", ctx -> "List of books");
+ * }
+ * }
+ * + *

Route Tracing (OtelHttpTracing)

+ * + *

While {@code OtelModule} bootstraps the core OpenTelemetry engine, it does not automatically + * trace web requests. You must explicitly include {@code OtelHttpTracing}. + * + *

Note that {@code OtelHttpTracing} is not an {@link + * io.jooby.opentelemetry.OtelExtension}; it is a native Jooby {@code Route.Filter}. It must be + * installed directly into the application's routing pipeline (e.g., via {@code use()}) to + * intercept, create, and propagate spans for incoming HTTP requests. + * + *

Manual Tracing

+ * + *

For tracing specific business logic, database queries, or external API calls, this module + * provides a fluent {@link io.jooby.opentelemetry.Trace} utility. You can retrieve it from the + * route context or inject it directly into your service layer to safely create, configure, and + * execute custom spans without risking context leaks. + * + *

{@code
+ * get("/books/{isbn}", ctx -> {
+ *   Trace trace = ctx.require(Trace.class);
+ *   String isbn = ctx.path("isbn").value();
+ *   return trace.span("fetch_book")
+ *        .attribute("isbn", isbn)
+ *        .execute(span -> {
+ *           span.addEvent("Executing database query");
+ *           return repository.findByIsbn(isbn);
+ *         });
+ * });
+ * }
+ * + *

Configuration

+ * + *

The OpenTelemetry SDK is configured directly from your application's {@code application.conf}. + * Any property defined inside the {@code otel} block is automatically extracted and used to + * configure the underlying SDK components, such as exporters, protocols, and service attributes. + * + *

{@code
+ * otel {
+ * service.name = "jooby-api"
+ * traces.exporter = otlp
+ * metrics.exporter = otlp
+ * logs.exporter = otlp
+ * exporter.otlp.protocol = grpc
+ * exporter.otlp.endpoint = "http://localhost:4317"
+ * }
+ * }
+ * + *

If no {@code otel} configuration block is present in the application configuration, the module + * will fall back to a baseline, default SDK. + * + *

Extensions Lifecycle

+ * + *

Additional OpenTelemetry integrations (such as logging appenders or connection pool metrics) + * are provided via {@link io.jooby.opentelemetry.OtelExtension} implementations. These extensions + * are not executed immediately upon module installation. + * + *

Instead, the module defers their execution by registering them to the application's {@code + * onStarting} lifecycle hook. This guarantees that the primary OpenTelemetry SDK is fully + * constructed, configured, and registered before any secondary extensions attempt to hook into it + * or emit telemetry data. + * + * @since 4.3.1 + * @author edgar + */ +module io.jooby.opentelemetry { + exports io.jooby.opentelemetry; + exports io.jooby.opentelemetry.instrumentation; + + requires io.jooby; + requires static com.github.spotbugs.annotations; + requires typesafe.config; + requires org.slf4j; + requires jul.to.slf4j; + requires io.opentelemetry.api; + requires io.opentelemetry.context; + requires io.opentelemetry.instrumentation.runtime_telemetry; + requires io.opentelemetry.sdk; + requires io.opentelemetry.sdk.autoconfigure; + + /* Hikari */ + requires static com.zaxxer.hikari; + requires static io.opentelemetry.instrumentation.hikaricp_3_0; + requires static java.sql; + + /* Logback */ + requires static ch.qos.logback.classic; + requires static io.opentelemetry.instrumentation.logback_appender_1_0; + /* Log4j */ + requires static io.opentelemetry.instrumentation.log4j_appender_2_17; + requires static org.apache.logging.log4j; + requires static org.apache.logging.log4j.core; + + /* Jetty */ + requires org.eclipse.jetty.server; + + /* Netty */ + requires static io.jooby.netty; + requires static io.netty.common; + requires static io.netty.buffer; + requires static io.netty.transport; + + /* Undertow */ + requires static undertow.core; + requires static xnio.api; + + /* Quartz */ + requires static org.quartz; + requires static io.opentelemetry.instrumentation.quartz_2_0; + + /* Db-Scheduler */ + requires static com.github.kagkarlsson.scheduler; + requires jakarta.inject; +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelHttpTracingTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelHttpTracingTest.java new file mode 100644 index 0000000000..1d34b687e5 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelHttpTracingTest.java @@ -0,0 +1,195 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; + +import io.jooby.Context; +import io.jooby.Route; +import io.jooby.Router; +import io.jooby.StatusCode; +import io.jooby.value.Value; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; + +public class OtelHttpTracingTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private Context ctx; + private Route route; + private Route.Handler next; + private Router router; + + @BeforeEach + void setUp() { + ctx = mock(Context.class); + route = mock(Route.class); + next = mock(Route.Handler.class); + router = mock(Router.class); + + // Core HTTP routing mocks + when(ctx.getMethod()).thenReturn("GET"); + when(ctx.getRequestPath()).thenReturn("/api/users/123"); + when(ctx.getRoute()).thenReturn(route); + when(route.getPattern()).thenReturn("/api/users/{id}"); + when(ctx.getRouter()).thenReturn(router); + + // OpenTelemetry DI mocks (injecting the in-memory SDK) + Tracer tracer = otelTesting.getOpenTelemetry().getTracer("test-tracer"); + when(ctx.require(Tracer.class)).thenReturn(tracer); + when(ctx.require(OpenTelemetry.class)).thenReturn(otelTesting.getOpenTelemetry()); + + // Header extraction mocks + Value missingHeader = mock(Value.class); + when(missingHeader.valueOrNull()).thenReturn(null); + when(ctx.header(anyString())).thenReturn(missingHeader); + } + + @Test + void shouldTraceSuccessfulRequest() throws Throwable { + // Arrange + when(next.apply(ctx)).thenReturn("Success"); + when(ctx.getResponseCode()).thenReturn(StatusCode.OK); + + OtelHttpTracing filter = new OtelHttpTracing(); + Route.Handler wrapped = filter.apply(next); + + // Act + Object result = wrapped.apply(ctx); + + // Trigger Jooby's onComplete callback + ArgumentCaptor onCompleteCaptor = ArgumentCaptor.forClass(Route.Complete.class); + verify(ctx).onComplete(onCompleteCaptor.capture()); + onCompleteCaptor.getValue().apply(ctx); + + // Assert + assertEquals("Success", result); + verify(ctx).setAttribute(any(String.class), any()); // Verifies span was put in context + + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + + SpanData span = spans.get(0); + assertEquals("GET /api/users/{id}", span.getName()); + assertEquals(SpanKind.SERVER, span.getKind()); + assertEquals(StatusData.unset(), span.getStatus()); + + assertThat(span.getAttributes().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.stringKey("http.request.method"), "GET") + .containsEntry( + io.opentelemetry.api.common.AttributeKey.stringKey("url.path"), "/api/users/123") + .containsEntry( + io.opentelemetry.api.common.AttributeKey.stringKey("http.route"), "/api/users/{id}") + .containsEntry( + io.opentelemetry.api.common.AttributeKey.longKey("http.response.status_code"), 200L); + } + + @Test + void shouldRecordExceptionAndFailSpan() throws Throwable { + // Arrange + RuntimeException exception = new RuntimeException("Database timeout"); + when(next.apply(ctx)).thenThrow(exception); + when(router.errorCode(exception)).thenReturn(StatusCode.SERVER_ERROR); + + OtelHttpTracing filter = new OtelHttpTracing(); + Route.Handler wrapped = filter.apply(next); + + // Act & Assert Exception + assertThrows(RuntimeException.class, () -> wrapped.apply(ctx)); + + // Notice we do NOT trigger onComplete here because Jooby handles exception propagation, + // but the catch block in the filter records the exception immediately. + // Span.end() relies on the container eventually triggering onComplete. For the sake of the + // test, + // we manually trigger it to finalize the span state as Jooby would. + when(ctx.getResponseCode()).thenReturn(StatusCode.SERVER_ERROR); + ArgumentCaptor onCompleteCaptor = ArgumentCaptor.forClass(Route.Complete.class); + verify(ctx).onComplete(onCompleteCaptor.capture()); + onCompleteCaptor.getValue().apply(ctx); + + // Assert Span + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + + SpanData span = spans.get(0); + assertEquals(StatusData.error(), span.getStatus()); + assertEquals(1, span.getEvents().size()); + assertEquals("exception", span.getEvents().get(0).getName()); + + assertThat(span.getAttributes().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.longKey("http.response.status_code"), 500L); + } + + @Test + void shouldMarkSpanAsErrorOn500StatusCode() throws Throwable { + // Arrange (Code executes fine, but sets a 500 status internally) + when(next.apply(ctx)).thenReturn("Internal Failure"); + when(ctx.getResponseCode()).thenReturn(StatusCode.SERVER_ERROR); + + OtelHttpTracing filter = new OtelHttpTracing(); + Route.Handler wrapped = filter.apply(next); + + // Act + wrapped.apply(ctx); + + ArgumentCaptor onCompleteCaptor = ArgumentCaptor.forClass(Route.Complete.class); + verify(ctx).onComplete(onCompleteCaptor.capture()); + onCompleteCaptor.getValue().apply(ctx); + + // Assert + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + + SpanData span = spans.get(0); + assertEquals(StatusData.error(), span.getStatus()); + assertThat(span.getAttributes().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.longKey("http.response.status_code"), 500L); + } + + @Test + void joobyRequestGetterExtractsHeaders() { + // Arrange + when(ctx.headerMap()) + .thenReturn( + Map.of("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")); + + Value mockHeaderValue = mock(Value.class); + when(mockHeaderValue.valueOrNull()) + .thenReturn("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + when(ctx.header("traceparent")).thenReturn(mockHeaderValue); + + // Act + Iterable keys = OtelHttpTracing.JoobyRequestGetter.INSTANCE.keys(ctx); + String headerVal = OtelHttpTracing.JoobyRequestGetter.INSTANCE.get(ctx, "traceparent"); + + // Assert + assertThat(keys).containsExactly("traceparent"); + assertEquals("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", headerVal); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelModuleTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelModuleTest.java new file mode 100644 index 0000000000..9bc43699a0 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/OtelModuleTest.java @@ -0,0 +1,87 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.jooby.Jooby; +import io.jooby.ServiceRegistry; +import io.jooby.SneakyThrows; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; + +@ExtendWith(MockitoExtension.class) +class OtelModuleTest { + + @Mock private Jooby application; + + @Mock private ServiceRegistry services; + + // 1. DO NOT use @Mock here. Use the official Noop implementation! + private final OpenTelemetry openTelemetry = OpenTelemetry.noop(); + + // 2. Extract the noop tracer so we can verify it gets registered + private final Tracer tracer = openTelemetry.getTracer("io.jooby.opentelemetry"); + + @BeforeEach + void setUp() { + // 3. We no longer need any MeterBuilder or Metric mocks. + // The Noop implementation handles all of that safely under the hood. + when(application.getServices()).thenReturn(services); + } + + @Test + @DisplayName("Should register OpenTelemetry and Tracer into Jooby services") + void shouldRegisterServices() { + OtelModule module = new OtelModule(openTelemetry); + module.install(application); + + verify(services).put(OpenTelemetry.class, openTelemetry); + verify(services).put(Tracer.class, tracer); + } + + @Test + @DisplayName("Should register RuntimeTelemetry onStop hook") + void shouldRegisterOnStopHooks() { + OtelModule module = new OtelModule(openTelemetry); + module.install(application); + + // Verify that application.onStop is called with the RuntimeTelemetry auto-closeable + verify(application).onStop(any(AutoCloseable.class)); + } + + @Test + @DisplayName("Should trigger nested extensions on application start") + void shouldTriggerExtensionsOnStarting() throws Exception { + OtelExtension mockExtension = mock(OtelExtension.class); + OtelModule module = new OtelModule(openTelemetry, mockExtension); + + // Capture the Runnable passed to application.onStarting + ArgumentCaptor runnableCaptor = + ArgumentCaptor.forClass(SneakyThrows.Runnable.class); + when(application.onStarting(runnableCaptor.capture())).thenReturn(application); + + module.install(application); + + // Execute the captured Runnable (simulating Jooby starting) + SneakyThrows.Runnable startingTask = runnableCaptor.getValue(); + startingTask.run(); + + // Verify the nested extension was executed with the correct application and OTel instance + verify(mockExtension).install(application, openTelemetry); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/TraceTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/TraceTest.java new file mode 100644 index 0000000000..9fffe54692 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/TraceTest.java @@ -0,0 +1,184 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; + +public class TraceTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private Trace trace; + + @BeforeEach + void setUp() { + // Clear any spans from previous tests + otelTesting.clearSpans(); + + // Inject the in-memory tracer + Tracer tracer = otelTesting.getOpenTelemetry().getTracer("test-tracer"); + trace = new Trace(tracer); + } + + @Test + void shouldExecuteSpanTaskAndReturnResult() throws Exception { + // Arrange + AttributeKey customKey = AttributeKey.stringKey("custom.typed"); + + // Act + String result = + trace + .span("db_query") + .attribute("str.key", "value") + .attribute("long.key", 42L) + .attribute("double.key", 3.14) + .attribute("bool.key", true) + .attribute(customKey, "typed-value") + .kind(SpanKind.CLIENT) + .rootContext() + .execute( + span -> { + span.addEvent("executing statement"); + return "success"; + }); + + // Assert Result + assertEquals("success", result); + + // Assert Span Data + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + + assertEquals("db_query", spanData.getName()); + assertEquals(SpanKind.CLIENT, spanData.getKind()); + assertFalse( + spanData.getParentSpanContext().isValid(), "Span should have no parent (rootContext)"); + assertEquals(StatusData.unset(), spanData.getStatus()); + + assertEquals(1, spanData.getEvents().size()); + assertEquals("executing statement", spanData.getEvents().get(0).getName()); + + assertThat(spanData.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("str.key"), "value") + .containsEntry(AttributeKey.longKey("long.key"), 42L) + .containsEntry(AttributeKey.doubleKey("double.key"), 3.14) + .containsEntry(AttributeKey.booleanKey("bool.key"), true) + .containsEntry(customKey, "typed-value"); + } + + @Test + void shouldExecuteSpanRunnableAndCloseSafely() throws Exception { + // Act + trace + .span("background_job") + .run( + span -> { + span.addEvent("job started"); + }); + + // Assert + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + + assertEquals("background_job", spanData.getName()); + assertEquals(SpanKind.INTERNAL, spanData.getKind()); // Default OTel kind + assertEquals(StatusData.unset(), spanData.getStatus()); + } + + @Test + void shouldRecordExceptionAndFailSpanInTask() { + // Act & Assert Exception Thrown + assertThatThrownBy( + () -> { + trace + .span("failing_task") + .execute( + span -> { + throw new IllegalStateException("Database connection failed"); + }); + }) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Database connection failed"); + + // Assert Span Data + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + + assertEquals("failing_task", spanData.getName()); + // Verifies status code and message were set correctly + assertEquals( + StatusData.create(StatusCode.ERROR, "Database connection failed"), spanData.getStatus()); + + // Verifies recordException(t) was called + assertEquals(1, spanData.getEvents().size()); + assertEquals("exception", spanData.getEvents().get(0).getName()); + } + + @Test + void shouldRecordExceptionWithNullMessage() { + // Act & Assert Exception Thrown + assertThatThrownBy( + () -> { + trace + .span("npe_task") + .run( + (Trace.SpanRunnable) + span -> { + throw new NullPointerException(); // NPEs typically have a null message + }); + }) + .isInstanceOf(NullPointerException.class); + + // Assert Span Data + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + + // Verifies fallback to class name when exception message is null + assertEquals( + StatusData.create(StatusCode.ERROR, "java.lang.NullPointerException"), + spanData.getStatus()); + } + + @Test + void shouldAllowUnderlyingConfigurationViaEscapeHatch() throws Exception { + // Act + trace + .span("configured_task") + .configure(builder -> builder.setAttribute("hatch.attr", "opened")) + .run( + span -> { + // Do nothing + }); + + // Assert + java.util.List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData spanData = spans.get(0); + + assertThat(spanData.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("hatch.attr"), "opened"); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelDbSchedulerTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelDbSchedulerTest.java new file mode 100644 index 0000000000..0641318cd3 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelDbSchedulerTest.java @@ -0,0 +1,171 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.github.kagkarlsson.scheduler.event.ExecutionChain; +import com.github.kagkarlsson.scheduler.task.CompletionHandler; +import com.github.kagkarlsson.scheduler.task.ExecutionContext; +import com.github.kagkarlsson.scheduler.task.TaskInstance; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.data.StatusData; + +public class OtelDbSchedulerTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private TaskInstance taskInstance; + private ExecutionContext executionContext; + private ExecutionChain chain; + private CompletionHandler completionHandler; + + private OtelDbScheduler interceptor; + + @BeforeEach + void setUp() { + otelTesting.clearSpans(); + otelTesting.clearMetrics(); + + taskInstance = mock(TaskInstance.class); + executionContext = mock(ExecutionContext.class); + chain = mock(ExecutionChain.class); + completionHandler = mock(CompletionHandler.class); + + when(taskInstance.getTaskName()).thenReturn("nightly-sync"); + when(taskInstance.getId()).thenReturn("sync-id-1234"); + + // Initialize the interceptor using the in-memory OpenTelemetry SDK + interceptor = new OtelDbScheduler(otelTesting.getOpenTelemetry()); + } + + @Test + void shouldTraceAndRecordMetricsOnSuccess() { + // Arrange + when(chain.proceed(taskInstance, executionContext)).thenAnswer(invocation -> completionHandler); + + // Act + Object result = interceptor.execute(taskInstance, executionContext, chain); + + // Assert Execution + assertEquals(completionHandler, result); + verify(chain).proceed(taskInstance, executionContext); + + // Assert Span + List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData span = spans.get(0); + + assertEquals("Job nightly-sync", span.getName()); + assertEquals(SpanKind.INTERNAL, span.getKind()); + assertEquals(StatusData.unset(), span.getStatus()); + assertThat(span.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("job.system"), "db-scheduler") + .containsEntry(AttributeKey.stringKey("job.id"), "sync-id-1234"); + + // Assert Metrics (Counter) + assertThat(otelTesting.getMetrics()) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("dbscheduler.task.completions"); + assertThat(metric.getLongSumData().getPoints()) + .anySatisfy( + point -> { + assertThat(point.getValue()).isEqualTo(1L); + assertThat(point.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("task"), "nightly-sync") + .containsEntry(AttributeKey.stringKey("result"), "ok"); + }); + }); + + // Assert Metrics (Histogram) + assertThat(otelTesting.getMetrics()) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("dbscheduler.task.duration"); + assertThat(metric.getHistogramData().getPoints()) + .anySatisfy( + point -> { + assertThat(point.getCount()).isEqualTo(1L); // 1 recorded event + assertThat(point.getSum()) + .isGreaterThanOrEqualTo(0.0); // duration in seconds + assertThat(point.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("task"), "nightly-sync") + .containsEntry(AttributeKey.stringKey("result"), "ok"); + }); + }); + } + + @Test + void shouldTraceAndRecordMetricsOnFailure() { + // Arrange + RuntimeException expectedException = new RuntimeException("Database timeout"); + when(chain.proceed(taskInstance, executionContext)).thenThrow(expectedException); + + // Act & Assert Exception + assertThatThrownBy(() -> interceptor.execute(taskInstance, executionContext, chain)) + .isInstanceOf(RuntimeException.class) + .hasMessage("Database timeout"); + + // Assert Span + List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + SpanData span = spans.get(0); + + assertEquals("Job nightly-sync", span.getName()); + assertEquals(StatusData.create(StatusCode.ERROR, ""), span.getStatus()); + + // Ensure exception was recorded as a span event + assertEquals(1, span.getEvents().size()); + assertEquals("exception", span.getEvents().get(0).getName()); + + // Assert Metrics (Counter marked as failed) + assertThat(otelTesting.getMetrics()) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("dbscheduler.task.completions"); + assertThat(metric.getLongSumData().getPoints()) + .anySatisfy( + point -> { + assertThat(point.getValue()).isEqualTo(1L); + assertThat(point.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("task"), "nightly-sync") + .containsEntry(AttributeKey.stringKey("result"), "failed"); + }); + }); + + // Assert Metrics (Histogram recorded despite failure) + assertThat(otelTesting.getMetrics()) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo("dbscheduler.task.duration"); + assertThat(metric.getHistogramData().getPoints()) + .anySatisfy( + point -> { + assertThat(point.getCount()).isEqualTo(1L); + assertThat(point.getAttributes().asMap()) + .containsEntry(AttributeKey.stringKey("task"), "nightly-sync") + .containsEntry(AttributeKey.stringKey("result"), "failed"); + }); + }); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelHikariTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelHikariTest.java new file mode 100644 index 0000000000..51b02eeee1 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelHikariTest.java @@ -0,0 +1,74 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.stubbing.OngoingStubbing; + +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.metrics.MetricsTrackerFactory; +import io.jooby.Jooby; +import io.jooby.Reified; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; + +public class OtelHikariTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private Jooby application; + private HikariDataSource primaryDataSource; + private HikariDataSource secondaryDataSource; + + @BeforeEach + void setUp() { + application = mock(Jooby.class); + primaryDataSource = mock(HikariDataSource.class); + secondaryDataSource = mock(HikariDataSource.class); + } + + @Test + void shouldInstrumentAllConfiguredDataSources() { + // Arrange + // Simulate a Jooby application with two separate database connections + List dataSources = Arrays.asList(primaryDataSource, secondaryDataSource); + + // Mock Jooby's Reified list resolution + OngoingStubbing> when = + when(application.require(Reified.list(HikariDataSource.class))); + when.thenReturn(dataSources); + + OtelHikari extension = new OtelHikari(); + + // Act + extension.install(application, otelTesting.getOpenTelemetry()); + + // Assert primary data source was instrumented + ArgumentCaptor captor1 = + ArgumentCaptor.forClass(MetricsTrackerFactory.class); + verify(primaryDataSource).setMetricsTrackerFactory(captor1.capture()); + assertNotNull( + captor1.getValue(), "MetricsTrackerFactory should be applied to primary data source"); + + // Assert secondary data source was instrumented + ArgumentCaptor captor2 = + ArgumentCaptor.forClass(MetricsTrackerFactory.class); + verify(secondaryDataSource).setMetricsTrackerFactory(captor2.capture()); + assertNotNull( + captor2.getValue(), "MetricsTrackerFactory should be applied to secondary data source"); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2Test.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2Test.java new file mode 100644 index 0000000000..d405298555 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLog4j2Test.java @@ -0,0 +1,119 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.slf4j.Logger; + +import io.jooby.Jooby; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.log4j.appender.v2_17.OpenTelemetryAppender; + +public class OtelLog4j2Test { + + private Jooby application; + private OpenTelemetry openTelemetry; + private Logger appLogger; + private MockedStatic mockedLogManager; + + @BeforeEach + void setUp() { + application = mock(Jooby.class); + openTelemetry = mock(OpenTelemetry.class); + appLogger = mock(Logger.class); + + when(application.getClassLoader()).thenReturn(Thread.currentThread().getContextClassLoader()); + when(application.getLog()).thenReturn(appLogger); + + // Intercept the static LogManager factory for this test thread + mockedLogManager = mockStatic(LogManager.class); + } + + @AfterEach + void tearDown() { + // Crucial: Always close static mocks to prevent them from leaking into other tests + mockedLogManager.close(); + } + + @Test + void shouldInstallAppenderWhenLog4jCoreIsPresent() { + // Arrange + LoggerContext loggerContext = mock(LoggerContext.class); + Configuration configuration = mock(Configuration.class); + LoggerConfig rootLoggerConfig = mock(LoggerConfig.class); + + when(loggerContext.getConfiguration()).thenReturn(configuration); + when(configuration.getRootLogger()).thenReturn(rootLoggerConfig); + + // Force LogManager to return our mocked core context + mockedLogManager + .when(() -> LogManager.getContext(any(ClassLoader.class), anyBoolean())) + .thenReturn(loggerContext); + + OtelLog4j2 extension = new OtelLog4j2(); + + // Act + extension.install(application, openTelemetry); + + // Assert Appender Registration + ArgumentCaptor appenderCaptor = + ArgumentCaptor.forClass(OpenTelemetryAppender.class); + + // 1. Verify appender was added to the global config + verify(configuration).addAppender(appenderCaptor.capture()); + OpenTelemetryAppender appender = appenderCaptor.getValue(); + assertNotNull(appender); + assertEquals("OpenTelemetry", appender.getName()); + + // 2. Verify appender was specifically attached to the Root Logger + verify(rootLoggerConfig).addAppender(eq(appender), eq(null), eq(null)); + + // 3. Verify Log4j2 was instructed to apply the changes + verify(loggerContext).updateLoggers(); + } + + @Test + void shouldLogWarningWhenLog4jCoreIsNotPresent() { + // Arrange + // Simulate a runtime where log4j-api is present, but routing to SimpleLogger instead of + // log4j-core + org.apache.logging.log4j.spi.LoggerContext simpleContext = + mock(org.apache.logging.log4j.spi.LoggerContext.class); + + mockedLogManager + .when(() -> LogManager.getContext(any(ClassLoader.class), anyBoolean())) + .thenReturn(simpleContext); + + OtelLog4j2 extension = new OtelLog4j2(); + + // Act + extension.install(application, openTelemetry); + + // Assert + verify(appLogger) + .warn( + "Log4j2OpenTelemetry requires log4j-core. Current context is: {}", + simpleContext.getClass().getName()); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLogbackTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLogbackTest.java new file mode 100644 index 0000000000..e63a6518c3 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelLogbackTest.java @@ -0,0 +1,102 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import io.jooby.Jooby; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; + +public class OtelLogbackTest { + + private Jooby application; + private OpenTelemetry openTelemetry; + private org.slf4j.Logger appLogger; + private MockedStatic mockedLoggerFactory; + + @BeforeEach + void setUp() { + application = mock(Jooby.class); + openTelemetry = mock(OpenTelemetry.class); + appLogger = mock(org.slf4j.Logger.class); + + when(application.getLog()).thenReturn(appLogger); + + // Intercept the static SLF4J LoggerFactory for this test thread + mockedLoggerFactory = mockStatic(LoggerFactory.class); + } + + @AfterEach + void tearDown() { + // Crucial: Always close static mocks to prevent them from breaking the test runner's own + // logging + mockedLoggerFactory.close(); + } + + @Test + void shouldInstallAppenderWhenLogbackIsPresent() { + // Arrange + LoggerContext loggerContext = mock(LoggerContext.class); + Logger rootLogger = mock(Logger.class); + + // Make the factory return our Logback context + mockedLoggerFactory.when(LoggerFactory::getILoggerFactory).thenReturn(loggerContext); + + // Wire up the root logger retrieval + when(loggerContext.getLogger(Logger.ROOT_LOGGER_NAME)).thenReturn(rootLogger); + + OtelLogback extension = new OtelLogback(); + + // Act + extension.install(application, openTelemetry); + + // Assert Appender Registration + ArgumentCaptor appenderCaptor = + ArgumentCaptor.forClass(OpenTelemetryAppender.class); + + verify(rootLogger).addAppender(appenderCaptor.capture()); + + OpenTelemetryAppender appender = appenderCaptor.getValue(); + assertEquals("OpenTelemetry", appender.getName()); + assertTrue( + appender.isStarted(), "The OpenTelemetryAppender should be started before being attached"); + } + + @Test + void shouldLogWarningWhenLogbackIsNotPresent() { + // Arrange + // Simulate an environment using a different SLF4J binding (like slf4j-simple) + ILoggerFactory simpleFactory = mock(ILoggerFactory.class); + mockedLoggerFactory.when(LoggerFactory::getILoggerFactory).thenReturn(simpleFactory); + + OtelLogback extension = new OtelLogback(); + + // Act + extension.install(application, openTelemetry); + + // Assert + verify(appLogger) + .warn( + "LogbackOpenTelemetry requires Logback. Current factory: {}", + simpleFactory.getClass().getName()); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelQuartzTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelQuartzTest.java new file mode 100644 index 0000000000..5f3c7d1f72 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelQuartzTest.java @@ -0,0 +1,64 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.quartz.ListenerManager; +import org.quartz.Scheduler; +import org.slf4j.Logger; + +import io.jooby.Jooby; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; + +public class OtelQuartzTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private Jooby application; + private Scheduler scheduler; + private ListenerManager listenerManager; + private Logger appLogger; + + @BeforeEach + void setUp() throws Exception { + application = mock(Jooby.class); + scheduler = mock(Scheduler.class); + listenerManager = mock(ListenerManager.class); + appLogger = mock(Logger.class); + + // Mock Jooby's registry lookup + when(application.require(Scheduler.class)).thenReturn(scheduler); + when(application.getLog()).thenReturn(appLogger); + + // OTel's QuartzTelemetry requires the ListenerManager to attach its JobListener. + // If we don't mock this, quartzTelemetry.configure(scheduler) will throw an NPE. + when(scheduler.getListenerManager()).thenReturn(listenerManager); + } + + @Test + void shouldInstallQuartzTelemetryListener() throws Exception { + // Arrange + OtelQuartz extension = new OtelQuartz(); + + // Act + extension.install(application, otelTesting.getOpenTelemetry()); + + // Assert + // 1. Verify we requested the Scheduler from Jooby + verify(application).require(Scheduler.class); + + // 2. Verify OpenTelemetry actually interacted with the Quartz Scheduler to hook its listener + verify(scheduler, times(2)).getListenerManager(); + + // 3. Verify our success debug log was fired + verify(appLogger).debug("OpenTelemetry Quartz JobListener installed."); + } +} diff --git a/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelServerMetricsTest.java b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelServerMetricsTest.java new file mode 100644 index 0000000000..fe5bc17849 --- /dev/null +++ b/modules/jooby-opentelemetry/src/test/java/io/jooby/opentelemetry/instrumentation/OtelServerMetricsTest.java @@ -0,0 +1,224 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.opentelemetry.instrumentation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; + +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.xnio.XnioWorker; +import org.xnio.management.XnioWorkerMXBean; + +import io.jooby.Jooby; +import io.jooby.Server; +import io.jooby.netty.NettyEventLoopGroup; +import io.netty.buffer.ByteBufAllocatorMetric; +import io.netty.buffer.ByteBufAllocatorMetricProvider; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.SingleThreadEventExecutor; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; + +public class OtelServerMetricsTest { + + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + + private Jooby application; + private Server server; + private Logger appLogger; + + @BeforeEach + void setUp() { + otelTesting.clearMetrics(); + + application = mock(Jooby.class); + server = mock(Server.class); + appLogger = mock(Logger.class); + + when(application.require(Server.class)).thenReturn(server); + when(application.getLog()).thenReturn(appLogger); + } + + @Test + void shouldLogDebugWhenServerIsUnknown() { + // Arrange + when(server.getName()).thenReturn("tomcat"); + OtelServerMetrics extension = new OtelServerMetrics(); + + // Act + extension.install(application, otelTesting.getOpenTelemetry()); + + // Assert + verify(appLogger).debug("No specific OTel metrics mapped for server: {}", "tomcat"); + assertThat(otelTesting.getMetrics()).isEmpty(); + } + + @Test + void shouldInstrumentJetty() { + // Arrange + when(server.getName()).thenReturn("jetty"); + + org.eclipse.jetty.server.Server jettyServer = mock(org.eclipse.jetty.server.Server.class); + QueuedThreadPool threadPool = mock(QueuedThreadPool.class); + ServerConnector connector = mock(ServerConnector.class); + + when(application.require(org.eclipse.jetty.server.Server.class)).thenReturn(jettyServer); + when(jettyServer.getThreadPool()).thenReturn(threadPool); + when(jettyServer.getConnectors()) + .thenReturn(new org.eclipse.jetty.server.Connector[] {connector}); + + // Mock Jetty Stats + when(threadPool.getBusyThreads()).thenReturn(42); + when(threadPool.getIdleThreads()).thenReturn(10); + when(threadPool.getQueueSize()).thenReturn(5); + when(connector.getConnectedEndPoints()) + .thenReturn(Collections.nCopies(100, null)); // Simulates 100 connections + + OtelServerMetrics extension = new OtelServerMetrics(); + + // Act + extension.install(application, otelTesting.getOpenTelemetry()); + + // Assert (Fetching metrics triggers the async callbacks) + assertGaugeValue("server.jetty.threads.active", 42.0); + assertGaugeValue("server.jetty.threads.idle", 10.0); + assertGaugeValue("server.jetty.queue.size", 5.0); + assertGaugeValue("server.jetty.connections.active", 100.0); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + void shouldInstrumentNetty() { + // Arrange + when(server.getName()).thenReturn("netty"); + + NettyEventLoopGroup nettyGroups = mock(NettyEventLoopGroup.class); + when(application.require(NettyEventLoopGroup.class)).thenReturn(nettyGroups); + + // --- 1. Mock Event Loop Group --- + EventLoopGroup eventLoopGroup = mock(EventLoopGroup.class); + when(nettyGroups.eventLoop()).thenReturn(eventLoopGroup); + + SingleThreadEventExecutor eventLoopExecutor = mock(SingleThreadEventExecutor.class); + when(eventLoopExecutor.pendingTasks()).thenReturn(15); + // EventLoopGroup implements Iterable + when(eventLoopGroup.iterator()) + .thenAnswer(i -> List.of(eventLoopExecutor).iterator()); + + // --- 2. Mock Acceptor Group (Different from Event Loop) --- + EventLoopGroup acceptorGroup = mock(EventLoopGroup.class); + when(nettyGroups.acceptor()).thenReturn(acceptorGroup); + + SingleThreadEventExecutor acceptorExecutor = mock(SingleThreadEventExecutor.class); + when(acceptorGroup.iterator()) + .thenAnswer(i -> List.of(acceptorExecutor).iterator()); + + // --- 3. Mock Worker (Using ThreadPoolExecutor scenario) --- + ThreadPoolExecutor workerPool = mock(ThreadPoolExecutor.class); + when(workerPool.getActiveCount()).thenReturn(30); + + // Mock the queue directly instead of trying to instantiate it with generic classes + BlockingQueue queue = mock(BlockingQueue.class); + when(queue.size()).thenReturn(7); + when(workerPool.getQueue()).thenReturn(queue); + + when(nettyGroups.worker()).thenReturn(workerPool); + + // --- 4. Mock ByteBufAllocator --- + // It must implement both ByteBufAllocator and ByteBufAllocatorMetricProvider + io.netty.buffer.ByteBufAllocator allocator = + mock( + io.netty.buffer.ByteBufAllocator.class, + withSettings().extraInterfaces(ByteBufAllocatorMetricProvider.class)); + ByteBufAllocatorMetric allocatorMetric = mock(ByteBufAllocatorMetric.class); + + when(((ByteBufAllocatorMetricProvider) allocator).metric()).thenReturn(allocatorMetric); + when(allocatorMetric.usedDirectMemory()).thenReturn(1024L); + when(allocatorMetric.usedHeapMemory()).thenReturn(2048L); + when(application.require(io.netty.buffer.ByteBufAllocator.class)).thenReturn(allocator); + + OtelServerMetrics extension = new OtelServerMetrics(); + + // Act + extension.install(application, otelTesting.getOpenTelemetry()); + + // Assert + assertGaugeValue("server.netty.eventloop.pending_tasks", 15.0); + assertGaugeValue("server.netty.eventloop.count", 1.0); + assertGaugeValue("server.netty.acceptor.count", 1.0); + assertGaugeValue("server.netty.worker.threads.active", 30.0); + assertGaugeValue("server.netty.worker.queue.size", 7.0); + assertGaugeValue("server.netty.memory.direct_used", 1024.0); + assertGaugeValue("server.netty.memory.heap_used", 2048.0); + } + + @Test + void shouldInstrumentUndertow() { + // Arrange + when(server.getName()).thenReturn("undertow"); + + io.undertow.Undertow undertow = mock(io.undertow.Undertow.class); + XnioWorker worker = mock(XnioWorker.class); + XnioWorkerMXBean mxBean = mock(XnioWorkerMXBean.class); + io.undertow.Undertow.ListenerInfo listenerInfo = mock(io.undertow.Undertow.ListenerInfo.class); + io.undertow.server.ConnectorStatistics stats = + mock(io.undertow.server.ConnectorStatistics.class); + + when(application.require(io.undertow.Undertow.class)).thenReturn(undertow); + when(undertow.getWorker()).thenReturn(worker); + when(worker.getMXBean()).thenReturn(mxBean); + when(undertow.getListenerInfo()).thenReturn(List.of(listenerInfo)); + when(listenerInfo.getConnectorStatistics()).thenReturn(stats); + + // Mock Undertow Stats + when(mxBean.getBusyWorkerThreadCount()).thenReturn(64); + when(mxBean.getWorkerQueueSize()).thenReturn(12); + when(mxBean.getIoThreadCount()).thenReturn(4); + when(stats.getActiveConnections()).thenReturn(250L); + + OtelServerMetrics extension = new OtelServerMetrics(); + + // Act + extension.install(application, otelTesting.getOpenTelemetry()); + + // Assert + assertGaugeValue("server.undertow.worker.threads.active", 64.0); + assertGaugeValue("server.undertow.worker.queue.size", 12.0); + assertGaugeValue("server.undertow.eventloop.count", 4.0); + assertGaugeValue("server.undertow.connections.active", 250.0); + } + + /** + * Helper method to locate a specific metric by name and assert its single DoubleGauge value. + * OpenTelemetry builds metrics as Doubles by default unless ofLongs() is explicitly called. + */ + private void assertGaugeValue(String metricName, double expectedValue) { + assertThat(otelTesting.getMetrics()) + .anySatisfy( + metric -> { + assertThat(metric.getName()).isEqualTo(metricName); + assertThat(metric.getDoubleGaugeData().getPoints()) + .anySatisfy( + point -> { + assertThat(point.getValue()).isEqualTo(expectedValue); + }); + }); + } +} diff --git a/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java b/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java index 26d1840595..fcae0a8667 100644 --- a/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java +++ b/modules/jooby-undertow/src/main/java/io/jooby/undertow/UndertowServer.java @@ -180,8 +180,12 @@ public Server start(@NonNull Jooby... application) { } else if (options.isHttpsOnly()) { throw new StartupException("Server configured for httpsOnly, but ssl options are not set"); } - fireStart(applications, worker); server = builder.build(); + for (var app : applications) { + app.getServices().put(Undertow.class, server); + } + fireStart(applications, worker); + server.start(); // --- EXTRACT OS-ASSIGNED PORTS --- diff --git a/modules/pom.xml b/modules/pom.xml index 407359fb36..fd2c073013 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -111,9 +111,12 @@ jooby-rxjava3 jooby-mutiny + + jooby-metrics + jooby-opentelemetry + jooby-whoops - jooby-metrics jooby-jasypt diff --git a/pom.xml b/pom.xml index 3e0a8abcfd..7c999bfb69 100644 --- a/pom.xml +++ b/pom.xml @@ -136,6 +136,7 @@ 0.13.0 6.4.0 2.5.2 + 16.7.1 9.2.1 8.17.0 1.12.797 diff --git a/tests/pom.xml b/tests/pom.xml index 042f426878..0a09c7384a 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -203,6 +203,12 @@ ${jooby.version} + + io.jooby + jooby-opentelemetry + ${jooby.version} + + io.jooby jooby-test From 8eaaef3867f62754921765d1aa09130bacacb430 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 13 Apr 2026 20:31:46 -0300 Subject: [PATCH 07/11] opentelemetry: add javadoc ref #3900 --- docs/asciidoc/modules/modules.adoc | 1 + docs/asciidoc/modules/opentelemetry.adoc | 362 +++++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 docs/asciidoc/modules/opentelemetry.adoc diff --git a/docs/asciidoc/modules/modules.adoc b/docs/asciidoc/modules/modules.adoc index ba9e6ab9fe..425bcf448f 100644 --- a/docs/asciidoc/modules/modules.adoc +++ b/docs/asciidoc/modules/modules.adoc @@ -38,6 +38,7 @@ Modules are distributed as separate dependencies. Below is the catalog of offici * link:{uiVersion}/#tooling-and-operations-development[Jooby Run]: Run and hot reload your application. * link:{uiVersion}/modules/whoops[Whoops]: Pretty page stacktrace reporter. * link:{uiVersion}/modules/metrics[Metrics]: Application metrics from the excellent metrics library. + * link:{uiVersion}/modules/opentelemetry[Open Telemetry]: Application metrics using Open Telemetry library. ==== Event Bus * link:{uiVersion}/modules/camel[Camel]: Camel module for Jooby. diff --git a/docs/asciidoc/modules/opentelemetry.adoc b/docs/asciidoc/modules/opentelemetry.adoc new file mode 100644 index 0000000000..d06e6fc9b4 --- /dev/null +++ b/docs/asciidoc/modules/opentelemetry.adoc @@ -0,0 +1,362 @@ +== OpenTelemetry + +The module provides the foundational engine for distributed tracing, metrics, and log correlation in your Jooby application. Its goal is to give you deep, vendor-neutral observability into your system. By integrating the https://opentelemetry.io/[OpenTelemetry] SDK, it automatically captures and exports telemetry data from HTTP requests, database connection pools, background jobs, and application logs. + +Because https://opentelemetry.io/[OpenTelemetry] is an open standard, you are not locked into a specific vendor. You can seamlessly route your telemetry data to any compatible APM, backend, or collector (such as SigNoz, DataDog, Jaeger, or Grafana) simply by changing your configuration properties. + +=== Usage + +1) Add the dependency: + +[dependency, artifactId="jooby-opentelemetry:OpenTelemetry Module"] +. + +2) Install and use OpenTelemetry: + +.Java +[source, java, role="primary"] +---- +import io.jooby.opentelemetry.OtelModule; +import io.jooby.opentelemetry.OtelHttpTracing; + +{ + install(new OtelModule()); <1> + + use(new OtelHttpTracing()); <2> + + get("/", ctx -> { + return "Hello OTel"; + }); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.opentelemetry.OtelModule +import io.jooby.opentelemetry.OtelHttpTracing + +{ + install(OtelModule()) <1> + + use(OtelHttpTracing()) <2> + + get("/") { ctx -> + "Hello OTel" + } +} +---- + +<1> Installs the core OpenTelemetry SDK engine. It **must be installed at the very beginning** of your application setup. +<2> Adds the `OtelHttpTracing` filter to automatically intercept, create, and propagate spans for incoming HTTP requests. + +[NOTE] +==== +**JVM Metrics:** Basic JVM operational metrics (such as memory usage, garbage collection times, and active thread counts) are automatically bound and exported by default the moment `OtelModule` is installed. +==== + +=== Exporters Configuration + +The OpenTelemetry SDK is completely driven by your application's configuration properties. Any property defined inside the `otel` block in your `application.conf` is automatically picked up by the SDK's auto-configuration engine. + +Here is how you can configure the exporters to send your data to various popular backends: + +==== SigNoz (or generic OTLP) +SigNoz natively accepts the standard OTLP (OpenTelemetry Protocol) format over gRPC. + +.application.conf +[source, properties] +---- +otel { + service.name = "jooby-api" + traces.exporter = otlp + metrics.exporter = otlp + logs.exporter = otlp + exporter.otlp.protocol = grpc + exporter.otlp.endpoint = "http://localhost:4317" +} +---- + +==== DataDog +To send data to DataDog, you typically use the OTLP HTTP protocol pointing to the DataDog Agent running on your infrastructure, or directly to their intake API. + +.application.conf +[source, properties] +---- +otel { + service.name = "jooby-api" + traces.exporter = otlp + metrics.exporter = otlp + logs.exporter = otlp + exporter.otlp.protocol = http/protobuf + exporter.otlp.endpoint = "http://localhost:4318" # Assuming local DataDog Agent + # If sending directly to DataDog, you would include the API key in headers: + # exporter.otlp.headers = "DD-API-KEY=your_api_key_here" +} +---- + +==== Jaeger +Jaeger also natively supports accepting OTLP data. + +.application.conf +[source, properties] +---- +otel { + service.name = "jooby-api" + traces.exporter = otlp + metrics.exporter = none # Jaeger is for traces only + logs.exporter = none # Jaeger is for traces only + exporter.otlp.protocol = grpc + exporter.otlp.endpoint = "http://localhost:4317" +} +---- + +=== Manual Tracing + +For tracing specific business logic, database queries, or external API calls deep within your service layer, this module provides an injectable `Trace` utility. + +You can retrieve it from the route context or inject it directly via DI to safely create and execute custom spans: + +.Manual Tracing +[source, java, role = "primary"] +---- +import io.jooby.opentelemetry.Trace; + +{ + get("/books/{isbn}", ctx -> { + Trace trace = require(Trace.class); + String isbn = ctx.path("isbn").value(); + + return trace.span("fetch_book") + .attribute("isbn", isbn) + .execute(span -> { + span.addEvent("Executing database query"); + return repository.findByIsbn(isbn); + }); + }); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.opentelemetry.Trace + +{ + get("/books/{isbn}") { ctx -> + val trace = require(Trace::class) + val isbn = ctx.path("isbn").value() + + trace.span("fetch_book") + .attribute("isbn", isbn) + .execute { span -> + span.addEvent("Executing database query") + repository.findByIsbn(isbn) + } + } +} +---- + +The `execute` and `run` blocks automatically handle the span context lifecycle, error recording, and finalization, ensuring no spans are leaked even if exceptions are thrown. + +=== Extensions + +Additional integrations are provided via `OtelExtension` implementations. Many of these rely on official OpenTelemetry instrumentation libraries, which you must add to your project's classpath. + +[NOTE] +==== +**Lifecycle & Lazy Initialization:** Although `OtelModule` must be installed at the very beginning of your application, its extensions are **lazily initialized**. They defer their execution to the application's `onStarting` lifecycle hook. This ensures that all target components provided by other modules (like database connection pools or background schedulers) are fully configured and available in the service registry before the OpenTelemetry extensions attempt to instrument them. +==== + +==== db-scheduler + +Automatically instruments the `db-scheduler` library. It tracks background task executions, measuring execution durations and recording successes and failures. + +.db-scheduler Integration +[source, java, role = "primary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelDbScheduler; + +{ + install(new DbSchedulerModule() + .withExecutionInterceptor(new OtelDbScheduler(require(OpenTelemetry.class))) + ); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelDbScheduler + +{ + install(DbSchedulerModule() + .withExecutionInterceptor(OtelDbScheduler(require(OpenTelemetry::class))) + ) +} +---- + +==== HikariCP + +Instruments all registered `HikariDataSource` instances to export critical pool metrics (active/idle connections, timeouts). + +Required dependency: +[dependency, groupId="io.opentelemetry.instrumentation", artifactId="opentelemetry-hikaricp-3.0", version="${otel-instrumentation.version}"] +. + +[NOTE] +==== +Installation order is critical. `OtelModule` must be installed **before** `HikariModule`. +==== + +.HikariCP Metrics +[source, java, role = "primary"] +---- +import io.jooby.hikari.HikariModule; +import io.jooby.opentelemetry.instrumentation.OtelHikari; + +{ + install(new OtelModule(new OtelHikari())); + + install(new HikariModule()); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.hikari.HikariModule +import io.jooby.opentelemetry.instrumentation.OtelHikari + +{ + install(OtelModule(OtelHikari())) + + install(HikariModule()) +} +---- + +==== Log4j2 + +Seamlessly exports all application logs to your OpenTelemetry backend, automatically correlated with active trace and span IDs using a dynamic appender. + +Required dependency: +[dependency, groupId="io.opentelemetry.instrumentation", artifactId="opentelemetry-log4j-appender-2.17", version="${otel-instrumentation.version}"] +. + +.Log4j2 Integration +[source, java, role = "primary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelLog4j2; + +{ + install(new OtelModule( + new OtelLog4j2() + )); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelLog4j2 + +{ + install(OtelModule( + OtelLog4j2() + )) +} +---- + +==== Logback + +Seamlessly exports all application logs to your OpenTelemetry backend, automatically correlated with active trace and span IDs using a dynamic appender. + +Required dependency: +[dependency, groupId="io.opentelemetry.instrumentation", artifactId="opentelemetry-logback-appender-1.0", version="${otel-instrumentation.version}"] +. + +.Logback Integration +[source, java, role = "primary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelLogback; + +{ + install(new OtelModule( + new OtelLogback() + )); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelLogback + +{ + install(OtelModule( + OtelLogback() + )) +} +---- + +==== Quartz + +Tracks background task executions handled by the Quartz scheduler, creating individual spans for each execution to monitor scheduling delays and execution durations. + +Required dependency: +[dependency, groupId="io.opentelemetry.instrumentation", artifactId="opentelemetry-quartz-2.0", version="${otel-instrumentation.version}"] +. + +.Quartz Integration +[source, java, role = "primary"] +---- +import io.jooby.quartz.QuartzModule; +import io.jooby.opentelemetry.instrumentation.OtelQuartz; + +{ + install(new OtelModule(new OtelQuartz())); + + install(new QuartzModule(MyJobs.class)); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.quartz.QuartzModule +import io.jooby.opentelemetry.instrumentation.OtelQuartz + +{ + install(OtelModule(OtelQuartz())) + + install(QuartzModule(MyJobs::class.java)) +} +---- + +==== Server Metrics + +Exports native, server-specific operational metrics. It automatically detects your underlying HTTP server (Jetty, Netty, or Undertow) and exports deep metrics like event loop pending tasks, thread pool sizes, and memory usage. + +.Server Metrics +[source, java, role = "primary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelServerMetrics; + +{ + install(new OtelModule( + new OtelServerMetrics() + )); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.opentelemetry.instrumentation.OtelServerMetrics + +{ + install(OtelModule( + OtelServerMetrics() + )) +} +---- From 6546b781488ed6b61d6346ad62485e6d925eeca0 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 13 Apr 2026 20:32:19 -0300 Subject: [PATCH 08/11] hikari: better display name for otel: connections --- .../src/main/java/io/jooby/hikari/HikariModule.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java b/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java index 29e851d15f..f82d232dd6 100644 --- a/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java +++ b/modules/jooby-hikari/src/main/java/io/jooby/hikari/HikariModule.java @@ -486,7 +486,8 @@ static HikariConfig build(Environment env, String database) { } // wake driver for otel if (dburl != null && dburl.startsWith("jdbc:otel:")) { - forceLoadDriver(databaseType(dburl.replace(":otel:", ":")), env); + dbtype = databaseType(dburl.replace(":otel:", ":")); + forceLoadDriver(dbtype, env); } if (dbtype == null) { String poolName = From 301962ebece54d88cda74d24f1c77f4032558a41 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 13 Apr 2026 20:39:36 -0300 Subject: [PATCH 09/11] doc: add opentelemetry to main features --- docs/asciidoc/index.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/asciidoc/index.adoc b/docs/asciidoc/index.adoc index d2d91d82ab..8498d1f2b3 100644 --- a/docs/asciidoc/index.adoc +++ b/docs/asciidoc/index.adoc @@ -60,6 +60,7 @@ fun main(args: Array) { * **Reactive Ready:** Support for <> (CompletableFuture, RxJava, Reactor, Mutiny, and Kotlin Coroutines). * **Server Choice:** Run on https://www.eclipse.org/jetty[Jetty], https://netty.io[Netty], https://vertx.io[Vert.x], or http://undertow.io[Undertow]. * **AI Ready:** Seamlessly expose your application's data and functions to Large Language Models (LLMs) using the first-class link:modules/mcp[Model Context Protocol (MCP)] module. +* **Deep Observability:** Native, vendor-neutral distributed tracing, server metrics, and log correlation via link:modules/opentelemetry[OpenTelemetry] module. * **Extensible:** Scale to a full-stack framework using extensions and link:modules[modules]. [TIP] From 7a396acc186bd4a775ddeb7f08007c208d620bfa Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 13 Apr 2026 20:47:09 -0300 Subject: [PATCH 10/11] build: release: close milestone when finish --- .github/workflows/maven-central.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/maven-central.yml b/.github/workflows/maven-central.yml index faf632cf7b..ace9352c2c 100644 --- a/.github/workflows/maven-central.yml +++ b/.github/workflows/maven-central.yml @@ -51,6 +51,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + issues: write # Required to update/close milestones steps: - uses: actions/checkout@v4 @@ -162,3 +163,25 @@ jobs: # Overwrite the existing release notes gh release edit $CURRENT_TAG --notes-file changelog.md + + - name: Close Milestone + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + CURRENT_TAG=${{ github.ref_name }} + VERSION_NUM=${CURRENT_TAG#v} + + # Fetch the ID of the milestone only if it is currently open + MILESTONE_ID=$(gh api repos/${{ github.repository }}/milestones -q ".[] | select(.title==\"$VERSION_NUM\" and .state==\"open\") | .number") + + if [ -n "$MILESTONE_ID" ]; then + echo "Closing milestone $VERSION_NUM (ID: $MILESTONE_ID)..." + gh api \ + --method PATCH \ + -H "Accept: application/vnd.github+json" \ + repos/${{ github.repository }}/milestones/$MILESTONE_ID \ + -f state='closed' + echo "Milestone closed successfully." + else + echo "No open milestone found matching version $VERSION_NUM. Skipping." + fi From ca79dae0e2cb9c541ee24a25e5bc2f056eeaf572 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Mon, 13 Apr 2026 20:55:57 -0300 Subject: [PATCH 11/11] v4.4.0 --- jooby/pom.xml | 2 +- modules/jooby-apt/pom.xml | 2 +- modules/jooby-avaje-inject/pom.xml | 2 +- modules/jooby-avaje-jsonb/pom.xml | 2 +- modules/jooby-avaje-validator/pom.xml | 2 +- modules/jooby-awssdk-v1/pom.xml | 2 +- modules/jooby-awssdk-v2/pom.xml | 2 +- modules/jooby-bom/pom.xml | 4 ++-- modules/jooby-caffeine/pom.xml | 2 +- modules/jooby-camel/pom.xml | 2 +- modules/jooby-cli/pom.xml | 2 +- modules/jooby-commons-email/pom.xml | 2 +- modules/jooby-conscrypt/pom.xml | 2 +- modules/jooby-db-scheduler/pom.xml | 2 +- modules/jooby-distribution/pom.xml | 2 +- modules/jooby-ebean/pom.xml | 2 +- modules/jooby-flyway/pom.xml | 2 +- modules/jooby-freemarker/pom.xml | 2 +- modules/jooby-gradle-setup/pom.xml | 2 +- modules/jooby-graphiql/pom.xml | 2 +- modules/jooby-graphql/pom.xml | 2 +- modules/jooby-grpc/pom.xml | 2 +- modules/jooby-gson/pom.xml | 2 +- modules/jooby-guice/pom.xml | 2 +- modules/jooby-handlebars/pom.xml | 2 +- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-hibernate/pom.xml | 2 +- modules/jooby-hikari/pom.xml | 2 +- modules/jooby-jackson/pom.xml | 2 +- modules/jooby-jackson3/pom.xml | 2 +- modules/jooby-jasypt/pom.xml | 2 +- modules/jooby-javadoc/pom.xml | 2 +- modules/jooby-jdbi/pom.xml | 2 +- modules/jooby-jetty/pom.xml | 2 +- modules/jooby-jsonrpc-avaje-jsonb/pom.xml | 2 +- modules/jooby-jsonrpc-jackson2/pom.xml | 2 +- modules/jooby-jsonrpc-jackson3/pom.xml | 2 +- modules/jooby-jsonrpc/pom.xml | 2 +- modules/jooby-jstachio/pom.xml | 2 +- modules/jooby-jte/pom.xml | 2 +- modules/jooby-jwt/pom.xml | 2 +- modules/jooby-kafka/pom.xml | 2 +- modules/jooby-kotlin/pom.xml | 2 +- modules/jooby-langchain4j/pom.xml | 2 +- modules/jooby-log4j/pom.xml | 2 +- modules/jooby-logback/pom.xml | 2 +- modules/jooby-maven-plugin/pom.xml | 2 +- modules/jooby-mcp-jackson2/pom.xml | 2 +- modules/jooby-mcp-jackson3/pom.xml | 2 +- modules/jooby-mcp/pom.xml | 2 +- modules/jooby-metrics/pom.xml | 2 +- modules/jooby-mutiny/pom.xml | 2 +- modules/jooby-netty/pom.xml | 2 +- modules/jooby-openapi/pom.xml | 2 +- modules/jooby-opentelemetry/pom.xml | 2 +- modules/jooby-pac4j/pom.xml | 2 +- modules/jooby-pebble/pom.xml | 2 +- modules/jooby-quartz/pom.xml | 2 +- modules/jooby-reactor/pom.xml | 2 +- modules/jooby-redis/pom.xml | 2 +- modules/jooby-redoc/pom.xml | 2 +- modules/jooby-rocker/pom.xml | 2 +- modules/jooby-run/pom.xml | 2 +- modules/jooby-rxjava3/pom.xml | 2 +- modules/jooby-stork/pom.xml | 2 +- modules/jooby-swagger-ui/pom.xml | 2 +- modules/jooby-test/pom.xml | 2 +- modules/jooby-thymeleaf/pom.xml | 2 +- modules/jooby-trpc-avaje-jsonb/pom.xml | 2 +- modules/jooby-trpc-generator/pom.xml | 2 +- modules/jooby-trpc-jackson2/pom.xml | 2 +- modules/jooby-trpc-jackson3/pom.xml | 2 +- modules/jooby-trpc/pom.xml | 2 +- modules/jooby-undertow/pom.xml | 2 +- modules/jooby-vertx-mysql-client/pom.xml | 2 +- modules/jooby-vertx-pg-client/pom.xml | 2 +- modules/jooby-vertx-sql-client/pom.xml | 2 +- modules/jooby-vertx/pom.xml | 2 +- modules/jooby-whoops/pom.xml | 2 +- modules/jooby-yasson/pom.xml | 2 +- modules/pom.xml | 2 +- pom.xml | 4 ++-- tests/pom.xml | 2 +- 83 files changed, 85 insertions(+), 85 deletions(-) diff --git a/jooby/pom.xml b/jooby/pom.xml index 021b84b2bb..a670de975f 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.3.1-SNAPSHOT + 4.4.0 jooby jooby diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index f7f10c34ca..3ea6d662de 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-apt jooby-apt diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml index 7a7e208488..17ca064c65 100644 --- a/modules/jooby-avaje-inject/pom.xml +++ b/modules/jooby-avaje-inject/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-avaje-inject jooby-avaje-inject diff --git a/modules/jooby-avaje-jsonb/pom.xml b/modules/jooby-avaje-jsonb/pom.xml index ea77a28996..3aafc86fe5 100644 --- a/modules/jooby-avaje-jsonb/pom.xml +++ b/modules/jooby-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-avaje-jsonb jooby-avaje-jsonb diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml index 6a391e84a6..e8d749d83b 100644 --- a/modules/jooby-avaje-validator/pom.xml +++ b/modules/jooby-avaje-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-avaje-validator jooby-avaje-validator diff --git a/modules/jooby-awssdk-v1/pom.xml b/modules/jooby-awssdk-v1/pom.xml index df4a7d40fe..53446b8f74 100644 --- a/modules/jooby-awssdk-v1/pom.xml +++ b/modules/jooby-awssdk-v1/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-awssdk-v1 jooby-awssdk-v1 diff --git a/modules/jooby-awssdk-v2/pom.xml b/modules/jooby-awssdk-v2/pom.xml index ba0fd7ec6d..fec8e8f120 100644 --- a/modules/jooby-awssdk-v2/pom.xml +++ b/modules/jooby-awssdk-v2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-awssdk-v2 jooby-awssdk-v2 diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index 99d5674bd2..f38e433dc9 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -7,14 +7,14 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 io.jooby jooby-bom jooby-bom pom - 4.3.1-SNAPSHOT + 4.4.0 Jooby (Bill of Materials) https://jooby.io diff --git a/modules/jooby-caffeine/pom.xml b/modules/jooby-caffeine/pom.xml index cf04bda03e..a8d5f4c68e 100644 --- a/modules/jooby-caffeine/pom.xml +++ b/modules/jooby-caffeine/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-caffeine jooby-caffeine diff --git a/modules/jooby-camel/pom.xml b/modules/jooby-camel/pom.xml index 65394c5c9c..3b820ee15c 100644 --- a/modules/jooby-camel/pom.xml +++ b/modules/jooby-camel/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-camel jooby-camel diff --git a/modules/jooby-cli/pom.xml b/modules/jooby-cli/pom.xml index 553dd008ea..a2260c353a 100644 --- a/modules/jooby-cli/pom.xml +++ b/modules/jooby-cli/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-cli jooby-cli diff --git a/modules/jooby-commons-email/pom.xml b/modules/jooby-commons-email/pom.xml index fd67d2da92..7130898da7 100644 --- a/modules/jooby-commons-email/pom.xml +++ b/modules/jooby-commons-email/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-commons-email jooby-commons-email diff --git a/modules/jooby-conscrypt/pom.xml b/modules/jooby-conscrypt/pom.xml index 870dd164e1..ce493773dd 100644 --- a/modules/jooby-conscrypt/pom.xml +++ b/modules/jooby-conscrypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-conscrypt jooby-conscrypt diff --git a/modules/jooby-db-scheduler/pom.xml b/modules/jooby-db-scheduler/pom.xml index bbdb059bf5..c19abef552 100644 --- a/modules/jooby-db-scheduler/pom.xml +++ b/modules/jooby-db-scheduler/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-db-scheduler jooby-db-scheduler diff --git a/modules/jooby-distribution/pom.xml b/modules/jooby-distribution/pom.xml index 90b371f136..55a54ccccb 100644 --- a/modules/jooby-distribution/pom.xml +++ b/modules/jooby-distribution/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-distribution jooby-distribution diff --git a/modules/jooby-ebean/pom.xml b/modules/jooby-ebean/pom.xml index 2c3ed0dadc..80c33ca4de 100644 --- a/modules/jooby-ebean/pom.xml +++ b/modules/jooby-ebean/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-ebean jooby-ebean diff --git a/modules/jooby-flyway/pom.xml b/modules/jooby-flyway/pom.xml index 0555d60a78..e7ea145864 100644 --- a/modules/jooby-flyway/pom.xml +++ b/modules/jooby-flyway/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-flyway jooby-flyway diff --git a/modules/jooby-freemarker/pom.xml b/modules/jooby-freemarker/pom.xml index ed5016c5ce..8c2ea0c49b 100644 --- a/modules/jooby-freemarker/pom.xml +++ b/modules/jooby-freemarker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-freemarker jooby-freemarker diff --git a/modules/jooby-gradle-setup/pom.xml b/modules/jooby-gradle-setup/pom.xml index 35f8752060..ee4b9781a4 100644 --- a/modules/jooby-gradle-setup/pom.xml +++ b/modules/jooby-gradle-setup/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-gradle-setup jooby-gradle-setup diff --git a/modules/jooby-graphiql/pom.xml b/modules/jooby-graphiql/pom.xml index 3122a7ab83..f551efe117 100644 --- a/modules/jooby-graphiql/pom.xml +++ b/modules/jooby-graphiql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-graphiql jooby-graphiql diff --git a/modules/jooby-graphql/pom.xml b/modules/jooby-graphql/pom.xml index 6d07951e05..0ce2b12a84 100644 --- a/modules/jooby-graphql/pom.xml +++ b/modules/jooby-graphql/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-graphql jooby-graphql diff --git a/modules/jooby-grpc/pom.xml b/modules/jooby-grpc/pom.xml index 08906bdabb..aac6daf692 100644 --- a/modules/jooby-grpc/pom.xml +++ b/modules/jooby-grpc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-grpc jooby-grpc diff --git a/modules/jooby-gson/pom.xml b/modules/jooby-gson/pom.xml index 674f735c2a..5f07b5c8ed 100644 --- a/modules/jooby-gson/pom.xml +++ b/modules/jooby-gson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-gson jooby-gson diff --git a/modules/jooby-guice/pom.xml b/modules/jooby-guice/pom.xml index ce8fadf0f7..343bf65749 100644 --- a/modules/jooby-guice/pom.xml +++ b/modules/jooby-guice/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-guice jooby-guice diff --git a/modules/jooby-handlebars/pom.xml b/modules/jooby-handlebars/pom.xml index cdb6fe78cd..17f9462245 100644 --- a/modules/jooby-handlebars/pom.xml +++ b/modules/jooby-handlebars/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-handlebars jooby-handlebars diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index e8cb48191e..60a33e0088 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-hibernate-validator jooby-hibernate-validator diff --git a/modules/jooby-hibernate/pom.xml b/modules/jooby-hibernate/pom.xml index 161100d3bb..3062bb3d87 100644 --- a/modules/jooby-hibernate/pom.xml +++ b/modules/jooby-hibernate/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-hibernate jooby-hibernate diff --git a/modules/jooby-hikari/pom.xml b/modules/jooby-hikari/pom.xml index 51efef5bad..9669af85d8 100644 --- a/modules/jooby-hikari/pom.xml +++ b/modules/jooby-hikari/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-hikari jooby-hikari diff --git a/modules/jooby-jackson/pom.xml b/modules/jooby-jackson/pom.xml index 959b100a9b..b39d58d22f 100644 --- a/modules/jooby-jackson/pom.xml +++ b/modules/jooby-jackson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jackson jooby-jackson diff --git a/modules/jooby-jackson3/pom.xml b/modules/jooby-jackson3/pom.xml index 908db0a760..2c9256eddd 100644 --- a/modules/jooby-jackson3/pom.xml +++ b/modules/jooby-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jackson3 jooby-jackson3 diff --git a/modules/jooby-jasypt/pom.xml b/modules/jooby-jasypt/pom.xml index 981aa518a9..99ad292dcf 100644 --- a/modules/jooby-jasypt/pom.xml +++ b/modules/jooby-jasypt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jasypt jooby-jasypt diff --git a/modules/jooby-javadoc/pom.xml b/modules/jooby-javadoc/pom.xml index eec2533dfa..6461cef604 100644 --- a/modules/jooby-javadoc/pom.xml +++ b/modules/jooby-javadoc/pom.xml @@ -8,7 +8,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-javadoc jooby-javadoc diff --git a/modules/jooby-jdbi/pom.xml b/modules/jooby-jdbi/pom.xml index b28c52bedc..bae8466406 100644 --- a/modules/jooby-jdbi/pom.xml +++ b/modules/jooby-jdbi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jdbi jooby-jdbi diff --git a/modules/jooby-jetty/pom.xml b/modules/jooby-jetty/pom.xml index baeda2a4bc..6679963c64 100644 --- a/modules/jooby-jetty/pom.xml +++ b/modules/jooby-jetty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jetty jooby-jetty diff --git a/modules/jooby-jsonrpc-avaje-jsonb/pom.xml b/modules/jooby-jsonrpc-avaje-jsonb/pom.xml index 2f21820190..b20f9700b1 100644 --- a/modules/jooby-jsonrpc-avaje-jsonb/pom.xml +++ b/modules/jooby-jsonrpc-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jsonrpc-avaje-jsonb diff --git a/modules/jooby-jsonrpc-jackson2/pom.xml b/modules/jooby-jsonrpc-jackson2/pom.xml index b0ba563e33..86a3742663 100644 --- a/modules/jooby-jsonrpc-jackson2/pom.xml +++ b/modules/jooby-jsonrpc-jackson2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jsonrpc-jackson2 diff --git a/modules/jooby-jsonrpc-jackson3/pom.xml b/modules/jooby-jsonrpc-jackson3/pom.xml index f98d0620f4..9147f675d3 100644 --- a/modules/jooby-jsonrpc-jackson3/pom.xml +++ b/modules/jooby-jsonrpc-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jsonrpc-jackson3 diff --git a/modules/jooby-jsonrpc/pom.xml b/modules/jooby-jsonrpc/pom.xml index db85c37da0..f41886404c 100644 --- a/modules/jooby-jsonrpc/pom.xml +++ b/modules/jooby-jsonrpc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jsonrpc diff --git a/modules/jooby-jstachio/pom.xml b/modules/jooby-jstachio/pom.xml index 6008b3152a..55d24a706a 100644 --- a/modules/jooby-jstachio/pom.xml +++ b/modules/jooby-jstachio/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jstachio jooby-jstachio diff --git a/modules/jooby-jte/pom.xml b/modules/jooby-jte/pom.xml index 140161c264..0e747d20da 100644 --- a/modules/jooby-jte/pom.xml +++ b/modules/jooby-jte/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jte jooby-jte diff --git a/modules/jooby-jwt/pom.xml b/modules/jooby-jwt/pom.xml index 679a5332d0..36fc42344b 100644 --- a/modules/jooby-jwt/pom.xml +++ b/modules/jooby-jwt/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-jwt jooby-jwt diff --git a/modules/jooby-kafka/pom.xml b/modules/jooby-kafka/pom.xml index 3513b66ff7..9c321608de 100644 --- a/modules/jooby-kafka/pom.xml +++ b/modules/jooby-kafka/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-kafka jooby-kafka diff --git a/modules/jooby-kotlin/pom.xml b/modules/jooby-kotlin/pom.xml index 0d32271bda..a6792185fa 100644 --- a/modules/jooby-kotlin/pom.xml +++ b/modules/jooby-kotlin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-kotlin jooby-kotlin diff --git a/modules/jooby-langchain4j/pom.xml b/modules/jooby-langchain4j/pom.xml index 013df74c96..13d5b59872 100644 --- a/modules/jooby-langchain4j/pom.xml +++ b/modules/jooby-langchain4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-langchain4j jooby-langchain4j diff --git a/modules/jooby-log4j/pom.xml b/modules/jooby-log4j/pom.xml index 439a973337..ece07b0467 100644 --- a/modules/jooby-log4j/pom.xml +++ b/modules/jooby-log4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-log4j jooby-log4j diff --git a/modules/jooby-logback/pom.xml b/modules/jooby-logback/pom.xml index b14ae0878e..bc8a78e91c 100644 --- a/modules/jooby-logback/pom.xml +++ b/modules/jooby-logback/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-logback jooby-logback diff --git a/modules/jooby-maven-plugin/pom.xml b/modules/jooby-maven-plugin/pom.xml index 9e8b59645b..49a2394795 100644 --- a/modules/jooby-maven-plugin/pom.xml +++ b/modules/jooby-maven-plugin/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-maven-plugin jooby-maven-plugin diff --git a/modules/jooby-mcp-jackson2/pom.xml b/modules/jooby-mcp-jackson2/pom.xml index b821e9e5db..203a30b6bb 100644 --- a/modules/jooby-mcp-jackson2/pom.xml +++ b/modules/jooby-mcp-jackson2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-mcp-jackson2 diff --git a/modules/jooby-mcp-jackson3/pom.xml b/modules/jooby-mcp-jackson3/pom.xml index b4cdd532da..7bb5be69a3 100644 --- a/modules/jooby-mcp-jackson3/pom.xml +++ b/modules/jooby-mcp-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-mcp-jackson3 diff --git a/modules/jooby-mcp/pom.xml b/modules/jooby-mcp/pom.xml index b4cc7e905c..a3d26e14ac 100644 --- a/modules/jooby-mcp/pom.xml +++ b/modules/jooby-mcp/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-mcp diff --git a/modules/jooby-metrics/pom.xml b/modules/jooby-metrics/pom.xml index e4688c0c0c..7ca95f6122 100644 --- a/modules/jooby-metrics/pom.xml +++ b/modules/jooby-metrics/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-metrics jooby-metrics diff --git a/modules/jooby-mutiny/pom.xml b/modules/jooby-mutiny/pom.xml index 39559439e6..3b2f06f22d 100644 --- a/modules/jooby-mutiny/pom.xml +++ b/modules/jooby-mutiny/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-mutiny jooby-mutiny diff --git a/modules/jooby-netty/pom.xml b/modules/jooby-netty/pom.xml index 0cff3f91f1..ccb13498b5 100644 --- a/modules/jooby-netty/pom.xml +++ b/modules/jooby-netty/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-netty jooby-netty diff --git a/modules/jooby-openapi/pom.xml b/modules/jooby-openapi/pom.xml index 25b5de170d..9212a8e488 100644 --- a/modules/jooby-openapi/pom.xml +++ b/modules/jooby-openapi/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-openapi jooby-openapi diff --git a/modules/jooby-opentelemetry/pom.xml b/modules/jooby-opentelemetry/pom.xml index f15ee123f1..bcd3ecd8f6 100644 --- a/modules/jooby-opentelemetry/pom.xml +++ b/modules/jooby-opentelemetry/pom.xml @@ -8,7 +8,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-opentelemetry jooby-opentelemetry diff --git a/modules/jooby-pac4j/pom.xml b/modules/jooby-pac4j/pom.xml index 84daf04744..8d62015fb6 100644 --- a/modules/jooby-pac4j/pom.xml +++ b/modules/jooby-pac4j/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-pac4j jooby-pac4j diff --git a/modules/jooby-pebble/pom.xml b/modules/jooby-pebble/pom.xml index b41583c653..30dde01216 100644 --- a/modules/jooby-pebble/pom.xml +++ b/modules/jooby-pebble/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-pebble jooby-pebble diff --git a/modules/jooby-quartz/pom.xml b/modules/jooby-quartz/pom.xml index de0709f41e..4703b1690e 100644 --- a/modules/jooby-quartz/pom.xml +++ b/modules/jooby-quartz/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-quartz jooby-quartz diff --git a/modules/jooby-reactor/pom.xml b/modules/jooby-reactor/pom.xml index 15422995ac..bf11f661a2 100644 --- a/modules/jooby-reactor/pom.xml +++ b/modules/jooby-reactor/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-reactor jooby-reactor diff --git a/modules/jooby-redis/pom.xml b/modules/jooby-redis/pom.xml index 385cafbed2..f87991e86d 100644 --- a/modules/jooby-redis/pom.xml +++ b/modules/jooby-redis/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-redis jooby-redis diff --git a/modules/jooby-redoc/pom.xml b/modules/jooby-redoc/pom.xml index 18d8f46e77..1126859665 100644 --- a/modules/jooby-redoc/pom.xml +++ b/modules/jooby-redoc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-redoc jooby-redoc diff --git a/modules/jooby-rocker/pom.xml b/modules/jooby-rocker/pom.xml index f7be523ad5..bcd6bda982 100644 --- a/modules/jooby-rocker/pom.xml +++ b/modules/jooby-rocker/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-rocker jooby-rocker diff --git a/modules/jooby-run/pom.xml b/modules/jooby-run/pom.xml index 3df890b956..17151621ae 100644 --- a/modules/jooby-run/pom.xml +++ b/modules/jooby-run/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-run jooby-run diff --git a/modules/jooby-rxjava3/pom.xml b/modules/jooby-rxjava3/pom.xml index 92b447eb82..b32e9e0585 100644 --- a/modules/jooby-rxjava3/pom.xml +++ b/modules/jooby-rxjava3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-rxjava3 jooby-rxjava3 diff --git a/modules/jooby-stork/pom.xml b/modules/jooby-stork/pom.xml index 76855c8cd2..fda27e73f5 100644 --- a/modules/jooby-stork/pom.xml +++ b/modules/jooby-stork/pom.xml @@ -4,7 +4,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-stork diff --git a/modules/jooby-swagger-ui/pom.xml b/modules/jooby-swagger-ui/pom.xml index a7543ba1de..eca336c752 100644 --- a/modules/jooby-swagger-ui/pom.xml +++ b/modules/jooby-swagger-ui/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-swagger-ui jooby-swagger-ui diff --git a/modules/jooby-test/pom.xml b/modules/jooby-test/pom.xml index 13ea00f072..4ea478701a 100644 --- a/modules/jooby-test/pom.xml +++ b/modules/jooby-test/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-test jooby-test diff --git a/modules/jooby-thymeleaf/pom.xml b/modules/jooby-thymeleaf/pom.xml index 2ab4772c7e..0a44abab18 100644 --- a/modules/jooby-thymeleaf/pom.xml +++ b/modules/jooby-thymeleaf/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-thymeleaf jooby-thymeleaf diff --git a/modules/jooby-trpc-avaje-jsonb/pom.xml b/modules/jooby-trpc-avaje-jsonb/pom.xml index 6876740527..1f915bec82 100644 --- a/modules/jooby-trpc-avaje-jsonb/pom.xml +++ b/modules/jooby-trpc-avaje-jsonb/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-trpc-avaje-jsonb diff --git a/modules/jooby-trpc-generator/pom.xml b/modules/jooby-trpc-generator/pom.xml index 995be1547c..306afcbb95 100644 --- a/modules/jooby-trpc-generator/pom.xml +++ b/modules/jooby-trpc-generator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-trpc-generator jooby-trpc-generator diff --git a/modules/jooby-trpc-jackson2/pom.xml b/modules/jooby-trpc-jackson2/pom.xml index d33a147d1e..7cc0bc0c42 100644 --- a/modules/jooby-trpc-jackson2/pom.xml +++ b/modules/jooby-trpc-jackson2/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-trpc-jackson2 diff --git a/modules/jooby-trpc-jackson3/pom.xml b/modules/jooby-trpc-jackson3/pom.xml index 1d67266a9f..3ff20393ea 100644 --- a/modules/jooby-trpc-jackson3/pom.xml +++ b/modules/jooby-trpc-jackson3/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-trpc-jackson3 diff --git a/modules/jooby-trpc/pom.xml b/modules/jooby-trpc/pom.xml index fb60b5cecb..ec33a6e551 100644 --- a/modules/jooby-trpc/pom.xml +++ b/modules/jooby-trpc/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-trpc jooby-trpc diff --git a/modules/jooby-undertow/pom.xml b/modules/jooby-undertow/pom.xml index 50af502501..49aecaa1ab 100644 --- a/modules/jooby-undertow/pom.xml +++ b/modules/jooby-undertow/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-undertow jooby-undertow diff --git a/modules/jooby-vertx-mysql-client/pom.xml b/modules/jooby-vertx-mysql-client/pom.xml index 99fdaaba4e..0fa73f0f48 100644 --- a/modules/jooby-vertx-mysql-client/pom.xml +++ b/modules/jooby-vertx-mysql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-vertx-mysql-client jooby-vertx-mysql-client diff --git a/modules/jooby-vertx-pg-client/pom.xml b/modules/jooby-vertx-pg-client/pom.xml index 04073fb53b..61a06bb614 100644 --- a/modules/jooby-vertx-pg-client/pom.xml +++ b/modules/jooby-vertx-pg-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-vertx-pg-client jooby-vertx-pg-client diff --git a/modules/jooby-vertx-sql-client/pom.xml b/modules/jooby-vertx-sql-client/pom.xml index 81d4d45b1f..6efb5d228b 100644 --- a/modules/jooby-vertx-sql-client/pom.xml +++ b/modules/jooby-vertx-sql-client/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-vertx-sql-client jooby-vertx-sql-client diff --git a/modules/jooby-vertx/pom.xml b/modules/jooby-vertx/pom.xml index 31da69a8d7..fa99dcb396 100644 --- a/modules/jooby-vertx/pom.xml +++ b/modules/jooby-vertx/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-vertx jooby-vertx diff --git a/modules/jooby-whoops/pom.xml b/modules/jooby-whoops/pom.xml index 6dd4e218c7..f973b4dabd 100644 --- a/modules/jooby-whoops/pom.xml +++ b/modules/jooby-whoops/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-whoops jooby-whoops diff --git a/modules/jooby-yasson/pom.xml b/modules/jooby-yasson/pom.xml index 66b85f5856..466acfb912 100644 --- a/modules/jooby-yasson/pom.xml +++ b/modules/jooby-yasson/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 4.3.1-SNAPSHOT + 4.4.0 jooby-yasson jooby-yasson diff --git a/modules/pom.xml b/modules/pom.xml index fd2c073013..5a83c963d3 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -4,7 +4,7 @@ io.jooby jooby-project - 4.3.1-SNAPSHOT + 4.4.0 modules diff --git a/pom.xml b/pom.xml index fba695798b..96333a13bb 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.jooby jooby-project - 4.3.1-SNAPSHOT + 4.4.0 pom jooby-project @@ -213,7 +213,7 @@ 21 21 yyyy-MM-dd HH:mm:ssa - 2026-04-08T17:05:01Z + 2026-04-13T23:55:50Z UTF-8 etc${file.separator}source${file.separator}formatter.sh diff --git a/tests/pom.xml b/tests/pom.xml index a2bd507813..44f81b8216 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -6,7 +6,7 @@ io.jooby jooby-project - 4.3.1-SNAPSHOT + 4.4.0 tests tests