From 19f46bd01f01dcc06173e90c11330951acee27f3 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Mon, 23 Dec 2024 01:05:35 +0100 Subject: [PATCH] add metrics collection for API - extract webclient call from getConfiguration and add metrics in DeviceConfigurationService - extract webclient call from getMeasurement, addMeasurement and add metrics in DeviceDataService - extract webclient call from getIdentityFromDevice and add metrics in DeviceRegistryService - add MeterRegistry dependency - fix test application.yml - update kotlin logging dependency --- README.md | 35 ++++++----- build.gradle.kts | 18 +++++- gradle.properties | 2 +- .../service/DeviceConfigurationService.kt | 39 +++++++++---- .../ltd/hlaeja/service/DeviceDataService.kt | 58 ++++++++++++++----- .../hlaeja/service/DeviceRegistryService.kt | 39 +++++++++---- .../kotlin/ltd/hlaeja/service/JwtService.kt | 4 +- src/main/kotlin/ltd/hlaeja/util/Helper.kt | 6 +- .../kotlin/ltd/hlaeja/util/WebClientCalls.kt | 56 ++++++++++++++++++ src/main/resources/application.yml | 38 ++++++++++++ src/test/resources/application.yml | 8 +++ 11 files changed, 244 insertions(+), 59 deletions(-) create mode 100644 src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt create mode 100644 src/test/resources/application.yml diff --git a/README.md b/README.md index 2a71510..cf2ed08 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,25 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose ## Properties for deployment -| name | required | info | -|-------------------------------|----------|--------------------------| -| spring.profiles.active | * | Spring Boot environment | -| server.port | * | HTTP port | -| server.ssl.enabled | * | HTTP Enable SSL | -| server.ssl.key-store | * | HTTP Keystore | -| server.ssl.key-store-type | * | HTTP Cert Type | -| server.ssl.key-store-password | ** | HTTP Cert Pass | -| jwt.public-key | * | JWT public key | -| device-registry.url | * | Device Register URL | -| device-data.url | * | Device Data URL | -| device-configuration.url | * | Device Configuration URL | +| name | required | info | +|----------------------------------------------|----------|----------------------------------------------| +| spring.profiles.active | * | Spring Boot environment | +| server.port | * | HTTP port | +| server.ssl.enabled | * | HTTP Enable SSL | +| server.ssl.key-store | * | HTTP Keystore | +| server.ssl.key-store-type | * | HTTP Cert Type | +| server.ssl.key-store-password | ** | HTTP Cert Pass | +| jwt.public-key | * | JWT public key | +| device-registry.url | * | Device Register URL | +| device-data.url | * | Device Data URL | +| device-configuration.url | * | Device Configuration URL | +| management.influx.metrics.export.api-version | | InfluxDB API version | +| management.influx.metrics.export.enabled | | Enable/Disable exporting metrics to InfluxDB | +| management.influx.metrics.export.bucket | * | InfluxDB bucket name | +| management.influx.metrics.export.org | * | InfluxDB organization | +| management.influx.metrics.export.token | ** | InfluxDB token | +| management.influx.metrics.export.uri | * | InfluxDB URL | +| management.metrics.tags.application | * | Application instance tag for metrics | Required: * can be stored as text, and ** need to be stored as secret. @@ -28,8 +35,8 @@ Run `release.sh` script from `master` branch. ### Developer Keystore 1. Open `hosts` file: - * On Unix-like systems (Linux, macOS), this directory is typically `/etc/hosts`. - * On Windows, this directory is typically `%SystemRoot%\System32\drivers\etc\hosts`. + * On Unix-like systems (Linux, macOS), this directory is typically `/etc/hosts`. + * On Windows, this directory is typically `%SystemRoot%\System32\drivers\etc\hosts`. 2. Add the following lines to the `hosts` file: ```text diff --git a/build.gradle.kts b/build.gradle.kts index 5fa5881..4ca4b1b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer + plugins { alias(hlaeja.plugins.kotlin.jvm) alias(hlaeja.plugins.kotlin.spring) @@ -13,6 +15,7 @@ dependencies { implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlinx.coroutines) + implementation(hlaeja.micrometer.registry.influx) implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.webflux) @@ -31,6 +34,17 @@ dependencies { group = "ltd.hlaeja" -tasks.named("processResources") { - dependsOn("copyCertificates") +fun influxDbToken(): String = config.findOrDefault("influxdb.token", "INFLUXDB_TOKEN", "") + +tasks { + named("containerCreate", DockerCreateContainer::class) { + withEnvVar("MANAGEMENT_INFLUX_METRICS_EXPORT_TOKEN", influxDbToken()) + } + withType { + filesMatching("**/application.yml") { filter { it.replace("%INFLUXDB_TOKEN%", influxDbToken()) } } + onlyIf { file("src/main/resources/application.yml").exists() } + } + named("processResources") { + dependsOn("copyCertificates") + } } diff --git a/gradle.properties b/gradle.properties index f80cd28..56dedfa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ kotlin.code.style=official org.gradle.jvmargs=-Xmx1g version=0.3.0-SNAPSHOT -catalog=0.6.0 +catalog=0.7.0-SNAPSHOT docker.port.expose=8443 container.port.expose=8443 container.port.host=9000 diff --git a/src/main/kotlin/ltd/hlaeja/service/DeviceConfigurationService.kt b/src/main/kotlin/ltd/hlaeja/service/DeviceConfigurationService.kt index 4eb8c8f..8462837 100644 --- a/src/main/kotlin/ltd/hlaeja/service/DeviceConfigurationService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/DeviceConfigurationService.kt @@ -1,28 +1,47 @@ package ltd.hlaeja.service +import io.github.oshai.kotlinlogging.KotlinLogging +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.MeterRegistry import java.util.UUID import ltd.hlaeja.library.deviceConfiguration.Node import ltd.hlaeja.property.DeviceConfigurationProperty -import ltd.hlaeja.util.logCall -import org.springframework.http.HttpStatus.NOT_FOUND -import org.springframework.http.HttpStatus.NO_CONTENT -import org.springframework.http.HttpStatus.REQUEST_TIMEOUT +import ltd.hlaeja.util.deviceConfigurationGetConfiguration +import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE import org.springframework.stereotype.Service +import org.springframework.web.ErrorResponseException import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.awaitBodyOrNull +import org.springframework.web.reactive.function.client.WebClientRequestException import org.springframework.web.server.ResponseStatusException +private val log = KotlinLogging.logger {} + @Service class DeviceConfigurationService( + meterRegistry: MeterRegistry, private val webClient: WebClient, private val deviceConfigurationProperty: DeviceConfigurationProperty, ) { + private val deviceConfigurationSuccess = Counter.builder("device.configuration.success") + .description("Number of successful device configuration calls") + .register(meterRegistry) + + private val deviceConfigurationFailure = Counter.builder("device.configuration.failure") + .description("Number of failed device configuration calls") + .register(meterRegistry) + suspend fun getConfiguration( node: UUID, - ): Node.Response = webClient.get() - .uri("${deviceConfigurationProperty.url}/node-$node".also(::logCall)) - .retrieve() - .onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } - .awaitBodyOrNull() ?: throw ResponseStatusException(REQUEST_TIMEOUT) + ): Node.Response = try { + webClient.deviceConfigurationGetConfiguration(node, deviceConfigurationProperty) + .also { deviceConfigurationSuccess.increment() } + } catch (e: ErrorResponseException) { + deviceConfigurationFailure.increment() + throw e + } catch (e: WebClientRequestException) { + deviceConfigurationFailure.increment() + log.error(e) { "Error device registry" } + throw ResponseStatusException(SERVICE_UNAVAILABLE) + } } diff --git a/src/main/kotlin/ltd/hlaeja/service/DeviceDataService.kt b/src/main/kotlin/ltd/hlaeja/service/DeviceDataService.kt index 9f790e1..aee3ff9 100644 --- a/src/main/kotlin/ltd/hlaeja/service/DeviceDataService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/DeviceDataService.kt @@ -1,39 +1,65 @@ package ltd.hlaeja.service +import io.github.oshai.kotlinlogging.KotlinLogging +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.MeterRegistry import java.util.UUID import ltd.hlaeja.library.deviceData.MeasurementData import ltd.hlaeja.property.DeviceDataProperty -import ltd.hlaeja.util.logCall -import org.springframework.http.HttpStatus.NOT_FOUND -import org.springframework.http.HttpStatus.NO_CONTENT -import org.springframework.http.HttpStatus.REQUEST_TIMEOUT +import ltd.hlaeja.util.deviceDataAddMeasurement +import ltd.hlaeja.util.deviceDataGetMeasurement +import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE +import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service +import org.springframework.web.ErrorResponseException import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.awaitBodilessEntity -import org.springframework.web.reactive.function.client.awaitBodyOrNull +import org.springframework.web.reactive.function.client.WebClientRequestException import org.springframework.web.server.ResponseStatusException +private val log = KotlinLogging.logger {} + @Service class DeviceDataService( + meterRegistry: MeterRegistry, private val webClient: WebClient, private val deviceDataProperty: DeviceDataProperty, ) { + private val deviceDataSuccess = Counter.builder("device.data.success") + .description("Number of successful device data calls") + .register(meterRegistry) + + private val deviceDataFailure = Counter.builder("device.data.failure") + .description("Number of failed device data calls") + .register(meterRegistry) + suspend fun getMeasurement( client: UUID, node: UUID, - ): MeasurementData.Response = webClient.get() - .uri("${deviceDataProperty.url}/client-$client/node-$node".also(::logCall)) - .retrieve() - .onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } - .awaitBodyOrNull() ?: throw ResponseStatusException(REQUEST_TIMEOUT) + ): MeasurementData.Response = try { + webClient.deviceDataGetMeasurement(client, node, deviceDataProperty) + .also { deviceDataSuccess.increment() } + } catch (e: ErrorResponseException) { + deviceDataFailure.increment() + throw e + } catch (e: WebClientRequestException) { + deviceDataFailure.increment() + log.error(e) { "Error device registry" } + throw ResponseStatusException(SERVICE_UNAVAILABLE) + } suspend fun addMeasurement( client: UUID, request: MeasurementData.Request, - ) = webClient.post() - .uri("${deviceDataProperty.url}/client-$client".also(::logCall)) - .bodyValue(request) - .retrieve() - .awaitBodilessEntity() + ): ResponseEntity = try { + webClient.deviceDataAddMeasurement(client, request, deviceDataProperty) + .also { deviceDataSuccess.increment() } + } catch (e: ErrorResponseException) { + deviceDataFailure.increment() + throw e + } catch (e: WebClientRequestException) { + deviceDataFailure.increment() + log.error(e) { "Error device registry" } + throw ResponseStatusException(SERVICE_UNAVAILABLE) + } } diff --git a/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt b/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt index 397e1a0..ed755b2 100644 --- a/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/DeviceRegistryService.kt @@ -1,28 +1,47 @@ package ltd.hlaeja.service +import io.github.oshai.kotlinlogging.KotlinLogging +import io.micrometer.core.instrument.Counter +import io.micrometer.core.instrument.MeterRegistry import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.property.DeviceRegistryProperty -import ltd.hlaeja.util.logCall -import org.springframework.http.HttpStatus.NOT_ACCEPTABLE -import org.springframework.http.HttpStatus.NOT_FOUND -import org.springframework.http.HttpStatus.REQUEST_TIMEOUT +import ltd.hlaeja.util.deviceRegistryIdentityDevice +import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE import org.springframework.stereotype.Service +import org.springframework.web.ErrorResponseException import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.awaitBodyOrNull +import org.springframework.web.reactive.function.client.WebClientRequestException import org.springframework.web.server.ResponseStatusException +private val log = KotlinLogging.logger {} + @Service class DeviceRegistryService( + meterRegistry: MeterRegistry, private val webClient: WebClient, private val deviceRegistryProperty: DeviceRegistryProperty, ) { + private val identityDeviceSuccess = Counter.builder("device.identity.success") + .description("Number of successful device identity calls") + .register(meterRegistry) + + private val identityDeviceFailure = Counter.builder("device.identity.failure") + .description("Number of failed device identity calls") + .register(meterRegistry) + suspend fun getIdentityFromDevice( device: UUID, - ): Identity.Response = webClient.get() - .uri("${deviceRegistryProperty.url}/identity/device-$device".also(::logCall)) - .retrieve() - .onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_ACCEPTABLE) } - .awaitBodyOrNull() ?: throw ResponseStatusException(REQUEST_TIMEOUT) + ): Identity.Response = try { + webClient.deviceRegistryIdentityDevice(device, deviceRegistryProperty) + .also { identityDeviceSuccess.increment() } + } catch (e: ErrorResponseException) { + identityDeviceFailure.increment() + throw e + } catch (e: WebClientRequestException) { + identityDeviceFailure.increment() + log.error(e) { "Error device identity" } + throw ResponseStatusException(SERVICE_UNAVAILABLE) + } } diff --git a/src/main/kotlin/ltd/hlaeja/service/JwtService.kt b/src/main/kotlin/ltd/hlaeja/service/JwtService.kt index 4ba3c81..1697a1e 100644 --- a/src/main/kotlin/ltd/hlaeja/service/JwtService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/JwtService.kt @@ -1,12 +1,12 @@ package ltd.hlaeja.service +import io.github.oshai.kotlinlogging.KotlinLogging import io.jsonwebtoken.JwtParser import io.jsonwebtoken.Jwts import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.property.JwtProperty import ltd.hlaeja.util.PublicKeyProvider -import mu.KotlinLogging import org.springframework.stereotype.Service private val log = KotlinLogging.logger {} @@ -30,5 +30,5 @@ class JwtService( identity: String, ): UUID = parser.parseSignedClaims(identity) .let { UUID.fromString(it.payload["device"] as String) } - .also { log.debug("Identified client device: {}", it) } + .also { log.debug { "Identified client device: $it" } } } diff --git a/src/main/kotlin/ltd/hlaeja/util/Helper.kt b/src/main/kotlin/ltd/hlaeja/util/Helper.kt index f77be8e..64b7734 100644 --- a/src/main/kotlin/ltd/hlaeja/util/Helper.kt +++ b/src/main/kotlin/ltd/hlaeja/util/Helper.kt @@ -1,9 +1,7 @@ package ltd.hlaeja.util -import mu.KotlinLogging +import io.github.oshai.kotlinlogging.KotlinLogging private val log = KotlinLogging.logger {} -fun logCall(url: String) { - log.debug("calling: {}", url) -} +fun logCall(url: String) = log.debug { "calling: $url" } diff --git a/src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt b/src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt new file mode 100644 index 0000000..9626874 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/util/WebClientCalls.kt @@ -0,0 +1,56 @@ +package ltd.hlaeja.util + +import java.util.UUID +import ltd.hlaeja.library.deviceConfiguration.Node +import ltd.hlaeja.library.deviceData.MeasurementData +import ltd.hlaeja.library.deviceRegistry.Identity +import ltd.hlaeja.property.DeviceConfigurationProperty +import ltd.hlaeja.property.DeviceDataProperty +import ltd.hlaeja.property.DeviceRegistryProperty +import org.springframework.http.HttpStatus.NOT_ACCEPTABLE +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.NO_CONTENT +import org.springframework.http.HttpStatus.REQUEST_TIMEOUT +import org.springframework.http.ResponseEntity +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.awaitBodilessEntity +import org.springframework.web.reactive.function.client.awaitBodyOrNull +import org.springframework.web.server.ResponseStatusException + +suspend fun WebClient.deviceRegistryIdentityDevice( + device: UUID, + property: DeviceRegistryProperty, +): Identity.Response = get() + .uri("${property.url}/identity/device-$device".also(::logCall)) + .retrieve() + .onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_ACCEPTABLE) } + .awaitBodyOrNull() ?: throw ResponseStatusException(REQUEST_TIMEOUT) + +suspend fun WebClient.deviceDataGetMeasurement( + client: UUID, + node: UUID, + property: DeviceDataProperty, +): MeasurementData.Response = get() + .uri("${property.url}/client-$client/node-$node".also(::logCall)) + .retrieve() + .onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } + .awaitBodyOrNull() ?: throw ResponseStatusException(REQUEST_TIMEOUT) + +suspend fun WebClient.deviceDataAddMeasurement( + client: UUID, + request: MeasurementData.Request, + property: DeviceDataProperty, +): ResponseEntity = post() + .uri("${property.url}/client-$client".also(::logCall)) + .bodyValue(request) + .retrieve() + .awaitBodilessEntity() + +suspend fun WebClient.deviceConfigurationGetConfiguration( + node: UUID, + property: DeviceConfigurationProperty, +): Node.Response = get() + .uri("${property.url}/node-$node".also(::logCall)) + .retrieve() + .onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } + .awaitBodyOrNull() ?: throw ResponseStatusException(REQUEST_TIMEOUT) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index abdf84e..41309d1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -10,6 +10,25 @@ spring: name: "%APP_BUILD_OS_NAME%" version: "%APP_BUILD_OS_VERSION%" +management: + endpoints: + enabled-by-default: false + web: + exposure: + include: "health,info" + endpoint: + health: + enabled: true + show-details: always + info: + enabled: true + influx: + metrics: + export: + api-version: v2 + bucket: hlaeja + org: hlaeja_ltd + jwt: public-key: cert/public_key.pem @@ -22,6 +41,16 @@ spring: activate: on-profile: development +management: + metrics: + tags: + application: device-api + influx: + metrics: + export: + enabled: false + token: %INFLUXDB_TOKEN% + server: port: 8443 ssl: @@ -48,6 +77,15 @@ spring: activate: on-profile: docker +management: + metrics: + tags: + application: device-api + influx: + metrics: + export: + uri: http://InfluxDB:8086 + server: port: 8443 ssl: diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..d924ee7 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,8 @@ +jwt: + public-key: cert/valid-public-key.pem +device-registry: + url: http://localhost:9010 +device-data: + url: http://localhost:9020 +device-configuration: + url: http://localhost:9030