Skip to content

Commit 12943c4

Browse files
author
Robert Winkler
committed
Added OpenTelemetry Support
1 parent 3116d22 commit 12943c4

File tree

25 files changed

+433
-139
lines changed

25 files changed

+433
-139
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ subprojects {
6464
}
6565

6666
kotlin {
67-
jvmToolchain(17)
67+
jvmToolchain(21)
6868
}
6969

7070
}

kotlin-wot-binding-http/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ dependencies {
1414
implementation("io.ktor:ktor-serialization-jackson")
1515
implementation("io.ktor:ktor-server-metrics-micrometer")
1616
implementation("io.ktor:ktor-server-auto-head-response")
17+
18+
implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:2.13.1-alpha")
19+
1720
testImplementation("io.ktor:ktor-server-test-host")
1821
testImplementation("ch.qos.logback:logback-classic:1.5.12")
1922
testImplementation("com.marcinziolo:kotlin-wiremock:2.1.1")

kotlin-wot-binding-websocket/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
dependencies {
22
implementation(platform("io.ktor:ktor-bom:3.0.3"))
3+
34
api(project(":kotlin-wot"))
45
api(project(":kotlin-wot-lmos-protocol"))
56
implementation("org.slf4j:slf4j-api")
67
implementation("io.ktor:ktor-server-netty")
78
implementation("io.ktor:ktor-server-websockets")
89
implementation("io.ktor:ktor-client-websocket:1.1.4")
910
implementation("io.ktor:ktor-server-content-negotiation")
11+
12+
// Tracing
13+
implementation("io.opentelemetry.instrumentation:opentelemetry-ktor-3.0:2.13.1-alpha")
14+
1015
implementation("io.ktor:ktor-client-cio")
1116
implementation("io.ktor:ktor-client-auth")
1217
implementation("io.ktor:ktor-client-logging")

kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClient.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ class WebSocketProtocolClient(
238238
}
239239
}
240240

241-
private suspend fun requestAndReply(form: WoTForm, message: WoTMessage, timeoutMillis: Long = 10000L): Content {
241+
private suspend fun requestAndReply(form: WoTForm, message: WoTMessage, timeoutMillis: Long = 100000L): Content {
242242
val session = getOrCreateSession(form.href)
243243
val deferred = CompletableDeferred<Content>()
244244

kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolServer.kt

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ai.ancf.lmos.wot.thing.form.Form
1111
import ai.ancf.lmos.wot.thing.form.Operation
1212
import ai.ancf.lmos.wot.thing.schema.ContentListener
1313
import ai.ancf.lmos.wot.thing.schema.WoTExposedThing
14+
import ai.ancf.lmos.wot.tracing.withSpan
1415
import ai.anfc.lmos.wot.binding.ProtocolServer
1516
import ai.anfc.lmos.wot.binding.ProtocolServerException
1617
import com.fasterxml.jackson.databind.DeserializationFeature
@@ -31,6 +32,10 @@ import io.ktor.websocket.*
3132
import io.micrometer.core.instrument.binder.jvm.JvmGcMetrics
3233
import io.micrometer.core.instrument.binder.jvm.JvmMemoryMetrics
3334
import io.micrometer.core.instrument.binder.system.ProcessorMetrics
35+
import io.opentelemetry.api.trace.Span
36+
import io.opentelemetry.api.trace.SpanKind
37+
import io.opentelemetry.instrumentation.annotations.SpanAttribute
38+
import io.opentelemetry.instrumentation.annotations.WithSpan
3439
import org.slf4j.LoggerFactory
3540
import java.util.*
3641

@@ -154,11 +159,13 @@ class WebSocketProtocolServer(
154159

155160
// Default server setup
156161
fun defaultWebSocketServer(host: String, port: Int, servient: Servient): EmbeddedServer<*, *> {
162+
157163
return embeddedServer(Netty, port = port, host = host) {
158164
setupRoutingWithWebSockets(servient)
159165
}
160166
}
161167

168+
162169
fun Application.setupRoutingWithWebSockets(servient: Servient) {
163170
install(CallLogging)
164171
install(ContentNegotiation) {
@@ -196,41 +203,74 @@ fun Application.setupRoutingWithWebSockets(servient: Servient) {
196203
}
197204
}
198205

206+
199207
webSocket("/ws") {
200-
val sessionId = UUID.randomUUID().toString()
201-
202-
for (frame in incoming) {
203-
if (frame is Frame.Text) {
204-
try {
205-
// Deserialize the message to WoTMessage
206-
val message: WoTMessage = JsonMapper.instance.readValue(frame.readText())
207-
// Retrieve the thingId from the message
208-
val thingId = message.thingId
209-
val thing = servient.things[thingId]
210-
211-
if (thing == null) {
212-
sendError(thingId, message.messageId, ErrorType.THING_NOT_FOUND)
213-
return@webSocket
214-
}
215-
216-
// Handle the message based on its type
217-
when (message) {
218-
is ReadAllPropertiesMessage -> handleReadAllProperties(thing, message, thingId)
219-
is ReadPropertyMessage -> handleReadProperty(thing, message, thingId)
220-
is WritePropertyMessage -> handleWriteProperty(thing, message, thingId)
221-
is ObservePropertyMessage -> handleObserveProperty(thing, message, thingId, sessionId)
222-
is UnobservePropertyMessage -> handleUnobserveProperty(thing, message, thingId, sessionId)
223-
is InvokeActionMessage -> handleInvokeAction(thing, message, thingId)
224-
is SubscribeEventMessage -> handleSubscribeEvent(thing, message, thingId, sessionId)
225-
is UnsubscribeEventMessage -> handleUnsubscribeEvent(thing, message, thingId, sessionId)
226-
else -> sendError(thingId, message.messageId, ErrorType.UNSUPPORTED_MESSAGE_TYPE)
227-
}
228-
} catch (e: Exception) {
229-
val errorMessage = "Failed to read message"
230-
log.warn(errorMessage, e)
231-
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, errorMessage))
208+
val sessionId = this.call.request.headers["Sec-WebSocket-Key"] ?: UUID.randomUUID().toString()
209+
handleWebSocketSession(sessionId, servient)
210+
}
211+
}
212+
}
213+
214+
@WithSpan(kind = SpanKind.SERVER)
215+
suspend fun DefaultWebSocketServerSession.handleWebSocketSession(
216+
@SpanAttribute("websocket.session.id") sessionId: String,
217+
servient: Servient
218+
) {
219+
for (frame in incoming) {
220+
if (frame is Frame.Text) {
221+
try {
222+
withSpan("WebSocketProtocolServer.receiveMessage", {
223+
setSpanKind(SpanKind.SERVER)
224+
}) { span ->
225+
// Deserialize the message to WoTMessage
226+
val message: WoTMessage = JsonMapper.instance.readValue(frame.readText())
227+
// Retrieve the thingId from the message
228+
val thingId = message.thingId
229+
val thing = servient.things[thingId]
230+
231+
span.setAttribute("websocket.message.id", message.messageId)
232+
span.setAttribute("websocket.message.thing.id", thingId)
233+
span.setAttribute("websocket.session.id", sessionId)
234+
235+
if (thing == null) {
236+
sendError(thingId, message.messageId, ErrorType.THING_NOT_FOUND)
237+
return@withSpan
238+
}
239+
240+
// Handle the message based on its type
241+
when (message) {
242+
is ReadAllPropertiesMessage -> handleReadAllProperties(thing, message, thingId)
243+
is ReadPropertyMessage -> handleReadProperty(thing, message, thingId)
244+
is WritePropertyMessage -> handleWriteProperty(thing, message, thingId)
245+
is ObservePropertyMessage -> handleObserveProperty(
246+
thing,
247+
message,
248+
thingId,
249+
sessionId
250+
)
251+
252+
is UnobservePropertyMessage -> handleUnobserveProperty(
253+
thing,
254+
message,
255+
thingId,
256+
sessionId
257+
)
258+
259+
is InvokeActionMessage -> handleInvokeAction(thing, message, thingId)
260+
is SubscribeEventMessage -> handleSubscribeEvent(thing, message, thingId, sessionId)
261+
is UnsubscribeEventMessage -> handleUnsubscribeEvent(
262+
thing,
263+
message,
264+
thingId,
265+
sessionId
266+
)
267+
268+
else -> sendError(thingId, message.messageId, ErrorType.UNSUPPORTED_MESSAGE_TYPE)
232269
}
233270
}
271+
} catch (e: Exception) {
272+
val errorMessage = "Failed to read message"
273+
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, errorMessage))
234274
}
235275
}
236276
}
@@ -248,6 +288,9 @@ enum class ErrorType {
248288
UNSUPPORTED_MESSAGE_TYPE
249289
}
250290

291+
292+
293+
@WithSpan(kind = SpanKind.SERVER)
251294
suspend fun DefaultWebSocketServerSession.sendError(thingId: String, correlationId : String, errorType: ErrorType, message: String? = null) {
252295
val errorJson = when (errorType) {
253296
ErrorType.THING_NOT_FOUND -> createThingNotFound(thingId, correlationId)
@@ -262,6 +305,7 @@ suspend fun DefaultWebSocketServerSession.sendError(thingId: String, correlation
262305
sendSerialized(errorJson)
263306
}
264307

308+
265309
private suspend fun readProperty(
266310
thing: ExposedThing,
267311
propertyName: String,
@@ -275,7 +319,8 @@ private suspend fun readProperty(
275319
)
276320
}
277321

278-
suspend fun DefaultWebSocketServerSession.handleReadAllProperties(thing: ExposedThing, message: ReadAllPropertiesMessage, thingId: String) {
322+
@WithSpan(kind = SpanKind.SERVER)
323+
suspend fun DefaultWebSocketServerSession.handleReadAllProperties(thing: ExposedThing, message: ReadAllPropertiesMessage, @SpanAttribute("thingId") thingId: String) {
279324

280325
try {
281326
val propertyMap = thing.handleReadAllProperties().mapValues { entry ->
@@ -292,7 +337,8 @@ suspend fun DefaultWebSocketServerSession.handleReadAllProperties(thing: Exposed
292337
}
293338
}
294339

295-
suspend fun DefaultWebSocketServerSession.handleReadProperty(thing: ExposedThing, message: ReadPropertyMessage, thingId: String) {
340+
@WithSpan(kind = SpanKind.SERVER)
341+
suspend fun DefaultWebSocketServerSession.handleReadProperty(thing: ExposedThing, message: ReadPropertyMessage, @SpanAttribute("thingId") thingId: String) {
296342
val propertyName = message.property
297343
val property = thing.properties[propertyName]
298344

@@ -311,7 +357,8 @@ suspend fun DefaultWebSocketServerSession.handleReadProperty(thing: ExposedThing
311357
}
312358
}
313359

314-
suspend fun DefaultWebSocketServerSession.handleWriteProperty(thing: ExposedThing, message: WritePropertyMessage, thingId: String) {
360+
@WithSpan(kind = SpanKind.SERVER)
361+
suspend fun DefaultWebSocketServerSession.handleWriteProperty(thing: ExposedThing, message: WritePropertyMessage, @SpanAttribute("thingId")thingId: String) {
315362
val propertyName = message.property
316363
val data = message.data
317364
val property = thing.properties[propertyName]
@@ -330,8 +377,8 @@ suspend fun DefaultWebSocketServerSession.handleWriteProperty(thing: ExposedThin
330377
}
331378
}
332379
}
333-
334-
suspend fun DefaultWebSocketServerSession.handleObserveProperty(thing: ExposedThing, message: ObservePropertyMessage, thingId: String, sessionId: String) {
380+
@WithSpan(kind = SpanKind.SERVER)
381+
suspend fun DefaultWebSocketServerSession.handleObserveProperty(thing: ExposedThing, message: ObservePropertyMessage, @SpanAttribute("thingId") thingId: String, @SpanAttribute("sessionId") sessionId: String) {
335382
val propertyName = message.property
336383
val property = thing.properties[propertyName]
337384

@@ -360,8 +407,8 @@ suspend fun DefaultWebSocketServerSession.handleObserveProperty(thing: ExposedTh
360407
}
361408
}
362409
}
363-
364-
suspend fun DefaultWebSocketServerSession.handleUnobserveProperty(thing: ExposedThing, message: UnobservePropertyMessage, thingId: String, sessionId: String) {
410+
@WithSpan(kind = SpanKind.SERVER)
411+
suspend fun DefaultWebSocketServerSession.handleUnobserveProperty(thing: ExposedThing, message: UnobservePropertyMessage, @SpanAttribute("thingId") thingId: String, @SpanAttribute("sessionId") sessionId: String) {
365412
val propertyName = message.property
366413
val property = thing.properties[propertyName]
367414

@@ -377,11 +424,16 @@ suspend fun DefaultWebSocketServerSession.handleUnobserveProperty(thing: Exposed
377424
}
378425
}
379426
}
427+
@WithSpan(kind = SpanKind.SERVER)
428+
suspend fun DefaultWebSocketServerSession.handleInvokeAction(thing: ExposedThing, message: InvokeActionMessage, @SpanAttribute("thingId") thingId: String) {
429+
380430

381-
suspend fun DefaultWebSocketServerSession.handleInvokeAction(thing: ExposedThing, message: InvokeActionMessage, thingId: String) {
382431
val actionName = message.action
383432
val action = thing.actions[actionName]
384433

434+
Span.current().setAttribute("wot.thing.id", thingId)
435+
Span.current().setAttribute("wot.action.name", actionName)
436+
385437
if (action == null) {
386438
sendError(thingId, message.messageId, ErrorType.ACTION_NOT_FOUND, actionName)
387439
} else {
@@ -401,8 +453,8 @@ suspend fun DefaultWebSocketServerSession.handleInvokeAction(thing: ExposedThing
401453
}
402454
}
403455
}
404-
405-
suspend fun DefaultWebSocketServerSession.handleSubscribeEvent(thing: ExposedThing, message: SubscribeEventMessage, thingId: String, sessionId: String) {
456+
@WithSpan(kind = SpanKind.SERVER)
457+
suspend fun DefaultWebSocketServerSession.handleSubscribeEvent(thing: ExposedThing, message: SubscribeEventMessage, @SpanAttribute("thingId") thingId: String, @SpanAttribute("sessionId") sessionId: String) {
406458
val eventName = message.event
407459
val event = thing.events[eventName]
408460

@@ -431,8 +483,8 @@ suspend fun DefaultWebSocketServerSession.handleSubscribeEvent(thing: ExposedThi
431483
}
432484
}
433485
}
434-
435-
suspend fun DefaultWebSocketServerSession.handleUnsubscribeEvent(thing: ExposedThing, message: UnsubscribeEventMessage, thingId: String, sessionId: String) {
486+
@WithSpan(kind = SpanKind.SERVER)
487+
suspend fun DefaultWebSocketServerSession.handleUnsubscribeEvent(thing: ExposedThing, message: UnsubscribeEventMessage, @SpanAttribute("thingId") thingId: String, @SpanAttribute("sessionId") sessionId: String) {
436488
val eventName = message.event
437489
val event = thing.events[eventName]
438490

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import org.springframework.boot.gradle.tasks.bundling.BootJar
1+
2+
import org.springframework.boot.gradle.tasks.run.BootRun
3+
import java.net.URI
24

35
plugins {
46
kotlin("plugin.spring") version "1.9.10"
@@ -11,21 +13,77 @@ tasks.named<Test>("test") {
1113
}
1214

1315
dependencies {
16+
// Replace the following with the starter dependencies of specific modules you wish to use
1417
api(project(":kotlin-wot-binding-http"))
1518
api(project(":kotlin-wot-binding-websocket"))
1619
api(project(":kotlin-wot-binding-mqtt"))
1720
api(project(":kotlin-wot-spring-boot-starter"))
1821
api(project(":kotlin-wot-lmos-protocol"))
19-
implementation("ai.ancf.lmos:arc-azure-client:0.111.0")
20-
api("ai.ancf.lmos:arc-spring-boot-starter:0.111.0")
22+
implementation("org.eclipse.lmos:arc-azure-client:0.1.0-SNAPSHOT")
23+
24+
api("org.eclipse.lmos:arc-spring-boot-starter:0.1.0-SNAPSHOT")
25+
26+
//implementation("io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter:2.13.1")
27+
//implementation("com.azure:azure-core-metrics-opentelemetry:1.0.0-beta.27")
28+
//implementation("com.azure:azure-core-tracing-opentelemetry:1.0.0-beta.55")
29+
//implementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.13.1")
30+
implementation("io.opentelemetry.instrumentation:opentelemetry-logback-appender-1.0:2.13.1-alpha")
31+
//implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
32+
//implementation("io.opentelemetry:opentelemetry-exporter-otlp")
33+
34+
// AspectJ runtime for annotation-based AOP
35+
//implementation("org.aspectj:aspectjrt:1.9.9")
2136

22-
//implementation("dev.langchain4j:langchain4j-azure-open-ai:0.35.0")
37+
// AspectJ weaver for load-time weaving (LTW)
38+
//implementation("org.aspectj:aspectjweaver:1.9.9")
39+
//implementation("io.micrometer:micrometer-tracing-bridge-otel")
40+
//implementation("io.opentelemetry:opentelemetry-exporter-otlp")
41+
42+
implementation("dev.langchain4j:langchain4j-azure-open-ai:1.0.0-beta1")
2343
//implementation("dev.langchain4j:langchain4j:0.35.0")
2444
testImplementation("org.springframework.boot:spring-boot-starter-test")
2545
testImplementation("com.hivemq:hivemq-mqtt-client:1.3.3")
2646
implementation("org.testcontainers:testcontainers:1.20.3")
47+
48+
}
49+
50+
dependencyManagement {
51+
imports {
52+
mavenBom("io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom:2.13.1")
53+
}
54+
}
55+
56+
springBoot {
57+
mainClass.set("ai.ancf.lmos.wot.integration.AgentApplicationKt")
58+
}
59+
60+
tasks.register("downloadOtelAgent") {
61+
doLast {
62+
val agentUrl =
63+
"https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar"
64+
val agentFile = file("${project.buildDir}/libs/opentelemetry-javaagent.jar")
65+
66+
// Ensure directory exists before downloading
67+
agentFile.parentFile.mkdirs()
68+
69+
if (!agentFile.exists()) {
70+
println("Downloading OpenTelemetry Java Agent...")
71+
agentFile.writeBytes(URI(agentUrl).toURL().readBytes())
72+
println("Download completed: ${agentFile.absolutePath}")
73+
} else {
74+
println("OpenTelemetry Java Agent already exists: ${agentFile.absolutePath}")
75+
}
76+
}
2777
}
2878

29-
tasks.withType<BootJar> {
30-
mainClass.set("integration.AgentApplication")
79+
tasks.named<BootRun>("bootRun") {
80+
dependsOn("downloadOtelAgent")
81+
jvmArgs = listOf(
82+
"-javaagent:${project.buildDir}/libs/opentelemetry-javaagent.jar"
83+
)
84+
systemProperty("otel.java.global-autoconfigure.enabled", "true")
85+
systemProperty("otel.traces.exporter", "otlp")
86+
systemProperty("otel.exporter.otlp.endpoint", "http://localhost:4318")
87+
systemProperty("otel.service.name", "chat-agent")
88+
//systemProperty("otel.javaagent.debug", "true")
3189
}

0 commit comments

Comments
 (0)