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
This commit is contained in:
2024-12-23 01:05:35 +01:00
parent e48cf674a5
commit 19f46bd01f
11 changed files with 244 additions and 59 deletions

View File

@@ -5,7 +5,7 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose
## Properties for deployment ## Properties for deployment
| name | required | info | | name | required | info |
|-------------------------------|----------|--------------------------| |----------------------------------------------|----------|----------------------------------------------|
| spring.profiles.active | * | Spring Boot environment | | spring.profiles.active | * | Spring Boot environment |
| server.port | * | HTTP port | | server.port | * | HTTP port |
| server.ssl.enabled | * | HTTP Enable SSL | | server.ssl.enabled | * | HTTP Enable SSL |
@@ -16,6 +16,13 @@ Classes and endpoints, to shape and to steer, Devices and sensors, their purpose
| device-registry.url | * | Device Register URL | | device-registry.url | * | Device Register URL |
| device-data.url | * | Device Data URL | | device-data.url | * | Device Data URL |
| device-configuration.url | * | Device Configuration 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. Required: * can be stored as text, and ** need to be stored as secret.

View File

@@ -1,3 +1,5 @@
import com.bmuschko.gradle.docker.tasks.container.DockerCreateContainer
plugins { plugins {
alias(hlaeja.plugins.kotlin.jvm) alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring) alias(hlaeja.plugins.kotlin.spring)
@@ -13,6 +15,7 @@ dependencies {
implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.micrometer.registry.influx)
implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.library.hlaeja.common.messages)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
@@ -31,6 +34,17 @@ dependencies {
group = "ltd.hlaeja" group = "ltd.hlaeja"
tasks.named("processResources") { fun influxDbToken(): String = config.findOrDefault("influxdb.token", "INFLUXDB_TOKEN", "")
tasks {
named("containerCreate", DockerCreateContainer::class) {
withEnvVar("MANAGEMENT_INFLUX_METRICS_EXPORT_TOKEN", influxDbToken())
}
withType<ProcessResources> {
filesMatching("**/application.yml") { filter { it.replace("%INFLUXDB_TOKEN%", influxDbToken()) } }
onlyIf { file("src/main/resources/application.yml").exists() }
}
named("processResources") {
dependsOn("copyCertificates") dependsOn("copyCertificates")
} }
}

View File

@@ -1,7 +1,7 @@
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx1g org.gradle.jvmargs=-Xmx1g
version=0.3.0-SNAPSHOT version=0.3.0-SNAPSHOT
catalog=0.6.0 catalog=0.7.0-SNAPSHOT
docker.port.expose=8443 docker.port.expose=8443
container.port.expose=8443 container.port.expose=8443
container.port.host=9000 container.port.host=9000

View File

@@ -1,28 +1,47 @@
package ltd.hlaeja.service 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 java.util.UUID
import ltd.hlaeja.library.deviceConfiguration.Node import ltd.hlaeja.library.deviceConfiguration.Node
import ltd.hlaeja.property.DeviceConfigurationProperty import ltd.hlaeja.property.DeviceConfigurationProperty
import ltd.hlaeja.util.logCall import ltd.hlaeja.util.deviceConfigurationGetConfiguration
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.http.HttpStatus.NO_CONTENT
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT
import org.springframework.stereotype.Service 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.WebClient
import org.springframework.web.reactive.function.client.awaitBodyOrNull import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {}
@Service @Service
class DeviceConfigurationService( class DeviceConfigurationService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceConfigurationProperty: DeviceConfigurationProperty, 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( suspend fun getConfiguration(
node: UUID, node: UUID,
): Node.Response = webClient.get() ): Node.Response = try {
.uri("${deviceConfigurationProperty.url}/node-$node".also(::logCall)) webClient.deviceConfigurationGetConfiguration(node, deviceConfigurationProperty)
.retrieve() .also { deviceConfigurationSuccess.increment() }
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } } catch (e: ErrorResponseException) {
.awaitBodyOrNull<Node.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT) deviceConfigurationFailure.increment()
throw e
} catch (e: WebClientRequestException) {
deviceConfigurationFailure.increment()
log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
} }

View File

@@ -1,39 +1,65 @@
package ltd.hlaeja.service 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 java.util.UUID
import ltd.hlaeja.library.deviceData.MeasurementData import ltd.hlaeja.library.deviceData.MeasurementData
import ltd.hlaeja.property.DeviceDataProperty import ltd.hlaeja.property.DeviceDataProperty
import ltd.hlaeja.util.logCall import ltd.hlaeja.util.deviceDataAddMeasurement
import org.springframework.http.HttpStatus.NOT_FOUND import ltd.hlaeja.util.deviceDataGetMeasurement
import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service 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.WebClient
import org.springframework.web.reactive.function.client.awaitBodilessEntity import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.reactive.function.client.awaitBodyOrNull
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {}
@Service @Service
class DeviceDataService( class DeviceDataService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceDataProperty: DeviceDataProperty, 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( suspend fun getMeasurement(
client: UUID, client: UUID,
node: UUID, node: UUID,
): MeasurementData.Response = webClient.get() ): MeasurementData.Response = try {
.uri("${deviceDataProperty.url}/client-$client/node-$node".also(::logCall)) webClient.deviceDataGetMeasurement(client, node, deviceDataProperty)
.retrieve() .also { deviceDataSuccess.increment() }
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NO_CONTENT) } } catch (e: ErrorResponseException) {
.awaitBodyOrNull<MeasurementData.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT) deviceDataFailure.increment()
throw e
} catch (e: WebClientRequestException) {
deviceDataFailure.increment()
log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
suspend fun addMeasurement( suspend fun addMeasurement(
client: UUID, client: UUID,
request: MeasurementData.Request, request: MeasurementData.Request,
) = webClient.post() ): ResponseEntity<Void> = try {
.uri("${deviceDataProperty.url}/client-$client".also(::logCall)) webClient.deviceDataAddMeasurement(client, request, deviceDataProperty)
.bodyValue(request) .also { deviceDataSuccess.increment() }
.retrieve() } catch (e: ErrorResponseException) {
.awaitBodilessEntity() deviceDataFailure.increment()
throw e
} catch (e: WebClientRequestException) {
deviceDataFailure.increment()
log.error(e) { "Error device registry" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
} }

View File

@@ -1,28 +1,47 @@
package ltd.hlaeja.service 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 java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.DeviceRegistryProperty import ltd.hlaeja.property.DeviceRegistryProperty
import ltd.hlaeja.util.logCall import ltd.hlaeja.util.deviceRegistryIdentityDevice
import org.springframework.http.HttpStatus.NOT_ACCEPTABLE import org.springframework.http.HttpStatus.SERVICE_UNAVAILABLE
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.http.HttpStatus.REQUEST_TIMEOUT
import org.springframework.stereotype.Service 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.WebClient
import org.springframework.web.reactive.function.client.awaitBodyOrNull import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {}
@Service @Service
class DeviceRegistryService( class DeviceRegistryService(
meterRegistry: MeterRegistry,
private val webClient: WebClient, private val webClient: WebClient,
private val deviceRegistryProperty: DeviceRegistryProperty, 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( suspend fun getIdentityFromDevice(
device: UUID, device: UUID,
): Identity.Response = webClient.get() ): Identity.Response = try {
.uri("${deviceRegistryProperty.url}/identity/device-$device".also(::logCall)) webClient.deviceRegistryIdentityDevice(device, deviceRegistryProperty)
.retrieve() .also { identityDeviceSuccess.increment() }
.onStatus(NOT_FOUND::equals) { throw ResponseStatusException(NOT_ACCEPTABLE) } } catch (e: ErrorResponseException) {
.awaitBodyOrNull<Identity.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT) identityDeviceFailure.increment()
throw e
} catch (e: WebClientRequestException) {
identityDeviceFailure.increment()
log.error(e) { "Error device identity" }
throw ResponseStatusException(SERVICE_UNAVAILABLE)
}
} }

View File

@@ -1,12 +1,12 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.JwtParser import io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import java.util.UUID import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.property.JwtProperty import ltd.hlaeja.property.JwtProperty
import ltd.hlaeja.util.PublicKeyProvider import ltd.hlaeja.util.PublicKeyProvider
import mu.KotlinLogging
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -30,5 +30,5 @@ class JwtService(
identity: String, identity: String,
): UUID = parser.parseSignedClaims(identity) ): UUID = parser.parseSignedClaims(identity)
.let { UUID.fromString(it.payload["device"] as String) } .let { UUID.fromString(it.payload["device"] as String) }
.also { log.debug("Identified client device: {}", it) } .also { log.debug { "Identified client device: $it" } }
} }

View File

@@ -1,9 +1,7 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import mu.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
fun logCall(url: String) { fun logCall(url: String) = log.debug { "calling: $url" }
log.debug("calling: {}", url)
}

View File

@@ -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<Identity.Response>() ?: 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<MeasurementData.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT)
suspend fun WebClient.deviceDataAddMeasurement(
client: UUID,
request: MeasurementData.Request,
property: DeviceDataProperty,
): ResponseEntity<Void> = 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<Node.Response>() ?: throw ResponseStatusException(REQUEST_TIMEOUT)

View File

@@ -10,6 +10,25 @@ spring:
name: "%APP_BUILD_OS_NAME%" name: "%APP_BUILD_OS_NAME%"
version: "%APP_BUILD_OS_VERSION%" 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: jwt:
public-key: cert/public_key.pem public-key: cert/public_key.pem
@@ -22,6 +41,16 @@ spring:
activate: activate:
on-profile: development on-profile: development
management:
metrics:
tags:
application: device-api
influx:
metrics:
export:
enabled: false
token: %INFLUXDB_TOKEN%
server: server:
port: 8443 port: 8443
ssl: ssl:
@@ -48,6 +77,15 @@ spring:
activate: activate:
on-profile: docker on-profile: docker
management:
metrics:
tags:
application: device-api
influx:
metrics:
export:
uri: http://InfluxDB:8086
server: server:
port: 8443 port: 8443
ssl: ssl:

View File

@@ -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