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

- Add enableTelemetry config attribute to rbcs:server with default false

- Update Configuration DTO, XSD schema, Parser, Serializer, and all tests
This commit is contained in:
OpenCode
2026-04-28 14:59:08 +00:00
parent 70eccf83a8
commit ee7bc7e850
198 changed files with 11040 additions and 4 deletions

19
rbcs-servlet/.classpath Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/kotlin">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-25/"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.web.container"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

44
rbcs-servlet/.project Normal file
View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>rbcs-servlet</name>
<comment>Project rbcs-servlet created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.common.project.facet.core.builder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.wst.validation.validationbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1777370777023</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@@ -0,0 +1,2 @@
connection.project.dir=..
eclipse.preferences.version=1

View File

@@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=25
org.eclipse.jdt.core.compiler.compliance=25
org.eclipse.jdt.core.compiler.source=25

View File

@@ -0,0 +1,5 @@
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_4_0.xsd"
version="4.0">
</beans>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Context antiJARLocking="true">
<Resource name="BeanManager"
auth="Container"
type="javax.enterprise.inject.spi.BeanManager"
factory="org.jboss.weld.resources.ManagerObjectFactory"/>
</Context>

View File

@@ -0,0 +1,8 @@
handlers = java.util.logging.ConsoleHandler
.level=INFO
net.woggioni.rbcs.servlet.level=FINEST
java.util.logging.ConsoleHandler.level=INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format = %1$tF %1$tT [%4$s] %2$s %5$s %6$s%n
org.apache.catalina.core.ContainerBase.[Catalina].level=ALL
org.apache.catalina.core.ContainerBase.[Catalina].handlers=java.util.logging.ConsoleHandler

View File

@@ -0,0 +1,169 @@
package net.woggioni.rbcs.servlet
import jakarta.annotation.PreDestroy
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.servlet.annotation.WebServlet
import jakarta.servlet.http.HttpServlet
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import java.util.logging.Logger
import net.woggioni.jwo.HttpClient.HttpStatus
import net.woggioni.jwo.JWO
private class CacheKey(private val value: ByteArray) {
override fun equals(other: Any?) = if (other is CacheKey) {
value.contentEquals(other.value)
} else false
override fun hashCode() = value.contentHashCode()
}
@ApplicationScoped
open class InMemoryServletCache : AutoCloseable {
private val maxAge= Duration.ofDays(7)
private val maxSize = 0x8000000
companion object {
@JvmStatic
private val log = Logger.getLogger(this::class.java.name)
}
private val size = AtomicLong()
private val map = ConcurrentHashMap<CacheKey, ByteArray>()
private class RemovalQueueElement(val key: CacheKey, val value: ByteArray, val expiry: Instant) :
Comparable<RemovalQueueElement> {
override fun compareTo(other: RemovalQueueElement) = expiry.compareTo(other.expiry)
}
private val removalQueue = PriorityBlockingQueue<RemovalQueueElement>()
@Volatile
private var running = false
private val garbageCollector = Thread.ofVirtual().name("in-memory-cache-gc").start {
while (running) {
val el = removalQueue.poll(1, TimeUnit.SECONDS) ?: continue
val value = el.value
val now = Instant.now()
if (now > el.expiry) {
val removed = map.remove(el.key, value)
if (removed) {
updateSizeAfterRemoval(value)
}
} else {
removalQueue.put(el)
Thread.sleep(minOf(Duration.between(now, el.expiry), Duration.ofSeconds(1)))
}
}
}
private fun removeEldest(): Long {
while (true) {
val el = removalQueue.take()
val value = el.value
val removed = map.remove(el.key, value)
if (removed) {
val newSize = updateSizeAfterRemoval(value)
return newSize
}
}
}
private fun updateSizeAfterRemoval(removed: ByteArray): Long {
return size.updateAndGet { currentSize: Long ->
currentSize - removed.size
}
}
@PreDestroy
override fun close() {
running = false
garbageCollector.join()
}
open fun get(key: ByteArray) = map[CacheKey(key)]
open fun put(
key: ByteArray,
value: ByteArray,
) {
val cacheKey = CacheKey(key)
val oldSize = map.put(cacheKey, value)?.let { old ->
val result = old.size
result
} ?: 0
val delta = value.size - oldSize
var newSize = size.updateAndGet { currentSize: Long ->
currentSize + delta
}
removalQueue.put(RemovalQueueElement(cacheKey, value, Instant.now().plus(maxAge)))
while (newSize > maxSize) {
newSize = removeEldest()
}
}
}
@WebServlet(urlPatterns = ["/cache/*"])
class CacheServlet : HttpServlet() {
companion object {
@JvmStatic
private val log = Logger.getLogger(this::class.java.name)
}
@Inject
private lateinit var cache : InMemoryServletCache
private fun getKey(req : HttpServletRequest) : String {
return Path.of(req.pathInfo).fileName.toString()
}
override fun doPut(req: HttpServletRequest, resp: HttpServletResponse) {
val baos = ByteArrayOutputStream()
baos.use {
JWO.copy(req.inputStream, baos)
}
val key = getKey(req)
cache.put(key.toByteArray(Charsets.UTF_8), baos.toByteArray())
resp.status = 201
resp.setContentLength(0)
log.fine {
"[${Thread.currentThread().name}] Added value for key $key"
}
}
override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
val key = getKey(req)
val value = cache.get(key.toByteArray(Charsets.UTF_8))
if (value == null) {
log.fine {
"[${Thread.currentThread().name}] Cache miss for key $key"
}
resp.status = HttpStatus.NOT_FOUND.code
resp.setContentLength(0)
} else {
log.fine {
"[${Thread.currentThread().name}] Cache hit for key $key"
}
resp.status = HttpStatus.OK.code
resp.setContentLength(value.size)
ByteArrayInputStream(value).use {
JWO.copy(it, resp.outputStream)
}
}
}
}