From ffe84fd331ab23c82996f6ac70d50c8d246ba859 Mon Sep 17 00:00:00 2001 From: OpenCode Date: Tue, 28 Apr 2026 11:51:19 +0000 Subject: [PATCH] Add optional OpenTelemetry Netty server instrumentation - Update lys.version to 2026.04.14 - Add optional compileOnly dependency on opentelemetry-netty-4.1 in rbcs-server - Add runtime guard to only activate instrumentation when OTel classes are on classpath - Insert OTel combined handler after HttpServerCodec in the Netty pipeline - Add requires-static JPMS directives for optional module support --- gradle.properties | 2 +- .../net/woggioni/rbcs/api/Configuration.java | 3 + rbcs-server/build.gradle | 5 ++ rbcs-server/src/main/java/module-info.java | 5 ++ .../rbcs/server/RemoteBuildCacheServer.kt | 4 ++ .../rbcs/server/configuration/Parser.kt | 3 + .../rbcs/server/configuration/Serializer.kt | 1 + .../rbcs/server/otel/OtelIntegration.kt | 32 ++++++++++ .../rbcs/server/otel/OtelSdkIntegration.kt | 58 +++++++++++++++++++ .../net/woggioni/rbcs/server/rbcs-default.xml | 3 +- .../rbcs/server/schema/rbcs-server.xsd | 8 +++ .../test/AbstractBasicAuthServerTest.kt | 1 + .../rbcs/server/test/AbstractTlsServerTest.kt | 1 + .../rbcs/server/test/NoAuthServerTest.kt | 1 + 14 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelIntegration.kt create mode 100644 rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelSdkIntegration.kt diff --git a/gradle.properties b/gradle.properties index 2ef0284..f4ced78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.caching=true rbcs.version = 0.5.0 -lys.version = 2026.03.26 +lys.version = 2026.04.28 gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven docker.registry.url=gitea.woggioni.net diff --git a/rbcs-api/src/main/java/net/woggioni/rbcs/api/Configuration.java b/rbcs-api/src/main/java/net/woggioni/rbcs/api/Configuration.java index 0fe1e73..11857f6 100644 --- a/rbcs-api/src/main/java/net/woggioni/rbcs/api/Configuration.java +++ b/rbcs-api/src/main/java/net/woggioni/rbcs/api/Configuration.java @@ -16,6 +16,7 @@ import java.util.stream.Collectors; @Value public class Configuration { + boolean enableTelemetry; String host; int port; boolean proxyProtocolEnabled; @@ -150,6 +151,7 @@ public class Configuration { } public static Configuration of( + boolean enableTelemetry, String host, int port, boolean proxyProtocolEnabled, @@ -166,6 +168,7 @@ public class Configuration { Tls tls ) { return new Configuration( + enableTelemetry, host, port, proxyProtocolEnabled, diff --git a/rbcs-server/build.gradle b/rbcs-server/build.gradle index 91e5eb9..640fbc5 100644 --- a/rbcs-server/build.gradle +++ b/rbcs-server/build.gradle @@ -13,6 +13,11 @@ dependencies { implementation catalog.netty.buffer implementation catalog.netty.transport implementation catalog.netty.codec.haproxy + compileOnly catalog.opentelemetry.netty['4']['1'] + compileOnly catalog.opentelemetry.sdk.extension.autoconfigure + compileOnly catalog.opentelemetry.logback.appender['1']['0'] + compileOnly catalog.opentelemetry.extension.trace.propagators + compileOnly catalog.logback.classic api project(':rbcs-common') api project(':rbcs-api') diff --git a/rbcs-server/src/main/java/module-info.java b/rbcs-server/src/main/java/module-info.java index 627df67..9aaf4c1 100644 --- a/rbcs-server/src/main/java/module-info.java +++ b/rbcs-server/src/main/java/module-info.java @@ -17,6 +17,11 @@ module net.woggioni.rbcs.server { requires io.netty.common; requires io.netty.codec; requires io.netty.codec.haproxy; + requires static io.opentelemetry.api; + requires static io.opentelemetry.instrumentation.netty_4_1; + requires static io.opentelemetry.sdk.autoconfigure; + requires static io.opentelemetry.instrumentation.logback_appender_1_0; + requires static io.opentelemetry.extension.trace.propagation; requires org.slf4j; exports net.woggioni.rbcs.server; diff --git a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/RemoteBuildCacheServer.kt b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/RemoteBuildCacheServer.kt index e3d76f0..eb5f8d0 100644 --- a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/RemoteBuildCacheServer.kt +++ b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/RemoteBuildCacheServer.kt @@ -68,6 +68,8 @@ import net.woggioni.rbcs.common.Xml import net.woggioni.rbcs.common.createLogger import net.woggioni.rbcs.common.debug import net.woggioni.rbcs.common.info +import net.woggioni.rbcs.server.otel.OtelIntegration +import net.woggioni.rbcs.server.otel.OtelSdkIntegration import net.woggioni.rbcs.server.auth.AbstractNettyHttpAuthenticator import net.woggioni.rbcs.server.auth.Authorizer import net.woggioni.rbcs.server.auth.RoleAuthorizer @@ -431,6 +433,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) { maxChunkSize = cfg.connection.chunkSize } pipeline.addLast(HttpServerCodec(httpDecoderConfig)) + OtelIntegration.createHandler(cfg.isEnableTelemetry)?.let { pipeline.addLast(it) } pipeline.addLast(ReadTriggerDuplexHandler.NAME, ReadTriggerDuplexHandler()) pipeline.addLast(MaxRequestSizeHandler.NAME, MaxRequestSizeHandler(cfg.connection.maxRequestSize)) pipeline.addLast(HttpChunkContentCompressor(1024)) @@ -525,6 +528,7 @@ class RemoteBuildCacheServer(private val cfg: Configuration) { } fun run(): ServerHandle { + OtelSdkIntegration.initialize(cfg.isEnableTelemetry) // Create the multithreaded event loops for the server val bossGroup = MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()) val channelFactory = ChannelFactory { NioSocketChannel() } diff --git a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Parser.kt b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Parser.kt index f1ac5c6..355fc6e 100644 --- a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Parser.kt +++ b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Parser.kt @@ -46,6 +46,8 @@ object Parser { var groups = emptyMap() var tls: Tls? = null val serverPath = root.renderAttribute("path") + var enableTelemetry = root.renderAttribute("enable-telemetry") + ?.let(String::toBoolean) ?: false var incomingConnectionsBacklogSize = 1024 var authentication: Authentication? = null for (child in root.asIterable()) { @@ -233,6 +235,7 @@ object Parser { } } return Configuration.of( + enableTelemetry, host, port, proxyProtocolEnabled, diff --git a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Serializer.kt b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Serializer.kt index 67e78d3..f7506fb 100644 --- a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Serializer.kt +++ b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/configuration/Serializer.kt @@ -29,6 +29,7 @@ object Serializer { ?.let { serverPath -> attr("path", serverPath) } + attr("enable-telemetry", conf.isEnableTelemetry.toString()) node("bind") { attr("host", conf.host) attr("port", conf.port.toString()) diff --git a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelIntegration.kt b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelIntegration.kt new file mode 100644 index 0000000..fff39de --- /dev/null +++ b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelIntegration.kt @@ -0,0 +1,32 @@ +package net.woggioni.rbcs.server.otel + +import io.netty.channel.ChannelHandler +import io.opentelemetry.api.GlobalOpenTelemetry +import io.opentelemetry.instrumentation.netty.v4_1.NettyServerTelemetry +import net.woggioni.rbcs.common.createLogger +import net.woggioni.rbcs.common.warn + +object OtelIntegration { + + private val log = createLogger() + + val isAvailable: Boolean by lazy { + runCatching { + Class.forName("io.opentelemetry.api.OpenTelemetry") + }.fold( + onSuccess = { true }, + onFailure = { + log.warn { "OpenTelemetry classes not on classpath, instrumentation disabled" } + false + }, + ) + } + + fun createHandler(enabled: Boolean): ChannelHandler? { + return if (enabled && isAvailable) createHandlerInternal() else null + } + + private fun createHandlerInternal(): ChannelHandler { + return NettyServerTelemetry.create(GlobalOpenTelemetry.get()).createCombinedHandler() + } +} diff --git a/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelSdkIntegration.kt b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelSdkIntegration.kt new file mode 100644 index 0000000..f5ce7d4 --- /dev/null +++ b/rbcs-server/src/main/kotlin/net/woggioni/rbcs/server/otel/OtelSdkIntegration.kt @@ -0,0 +1,58 @@ +package net.woggioni.rbcs.server.otel + +import net.woggioni.rbcs.common.createLogger +import net.woggioni.rbcs.common.info + +object OtelSdkIntegration { + + private val log = createLogger() + + private val isAvailable: Boolean by lazy { + runCatching { + Class.forName("io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk") + }.fold( + onSuccess = { true }, + onFailure = { + log.info { "OpenTelemetry SDK autoconfigure not on classpath" } + false + }, + ) + } + + private val appenderAvailable: Boolean by lazy { + runCatching { + Class.forName("io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender") + }.fold( + onSuccess = { true }, + onFailure = { + log.info { "OpenTelemetry logback appender not on classpath" } + false + }, + ) + } + + fun initialize(enabled: Boolean) { + if (!enabled || !isAvailable) return + + log.info { "Initializing OpenTelemetry SDK with auto-configuration" } + + val sdk = io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk.builder() + .setResultAsGlobal() + .build() + .openTelemetrySdk + + if (appenderAvailable) { + runCatching { + val clazz = Class.forName("io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender") + clazz.getMethod("install", Class.forName("io.opentelemetry.api.OpenTelemetry")) + .invoke(null, sdk) + log.info { "OpenTelemetry logback appender installed" } + }.onFailure { ex -> + val msg = ex.localizedMessage ?: ex.javaClass.name + log.info { "Failed to install OpenTelemetry logback appender: $msg" } + } + } + + log.info { "OpenTelemetry SDK initialized successfully" } + } +} diff --git a/rbcs-server/src/main/resources/net/woggioni/rbcs/server/rbcs-default.xml b/rbcs-server/src/main/resources/net/woggioni/rbcs/server/rbcs-default.xml index 6d45ea1..55f3710 100644 --- a/rbcs-server/src/main/resources/net/woggioni/rbcs/server/rbcs-default.xml +++ b/rbcs-server/src/main/resources/net/woggioni/rbcs/server/rbcs-default.xml @@ -2,7 +2,8 @@ + xs:schemaLocation="urn:net.woggioni.rbcs.server jpms://net.woggioni.rbcs.server/net/woggioni/rbcs/server/schema/rbcs-server.xsd" + enable-telemetry="false"> \ No newline at end of file diff --git a/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd b/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd index 65aa616..b4c5399 100644 --- a/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd +++ b/rbcs-server/src/main/resources/net/woggioni/rbcs/server/schema/rbcs-server.xsd @@ -59,6 +59,14 @@ + + + + Enable OpenTelemetry distributed tracing for the server. + Even when enabled, telemetry only activates if OpenTelemetry classes are present on the classpath. + + + diff --git a/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractBasicAuthServerTest.kt b/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractBasicAuthServerTest.kt index 31d024c..acb5474 100644 --- a/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractBasicAuthServerTest.kt +++ b/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractBasicAuthServerTest.kt @@ -32,6 +32,7 @@ abstract class AbstractBasicAuthServerTest : AbstractServerTest() { override fun setUp() { this.cacheDir = testDir.resolve("cache") cfg = Configuration.of( + false, "127.0.0.1", getFreePort(), false, diff --git a/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractTlsServerTest.kt b/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractTlsServerTest.kt index ad8d660..f160c50 100644 --- a/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractTlsServerTest.kt +++ b/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/AbstractTlsServerTest.kt @@ -138,6 +138,7 @@ abstract class AbstractTlsServerTest : AbstractServerTest() { this.cacheDir = testDir.resolve("cache") createKeyStoreAndTrustStore() cfg = Configuration( + false, "127.0.0.1", getFreePort(), false, diff --git a/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/NoAuthServerTest.kt b/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/NoAuthServerTest.kt index d81f4d6..63b2a61 100644 --- a/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/NoAuthServerTest.kt +++ b/rbcs-server/src/test/kotlin/net/woggioni/rbcs/server/test/NoAuthServerTest.kt @@ -32,6 +32,7 @@ class NoAuthServerTest : AbstractServerTest() { override fun setUp() { this.cacheDir = testDir.resolve("cache") cfg = Configuration( + false, "127.0.0.1", getFreePort(), false,