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

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

View File

@@ -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<ProcessResources> {
filesMatching("**/application.yml") { filter { it.replace("%INFLUXDB_TOKEN%", influxDbToken()) } }
onlyIf { file("src/main/resources/application.yml").exists() }
}
named("processResources") {
dependsOn("copyCertificates")
}
}

View File

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

View File

@@ -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<Node.Response>() ?: 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)
}
}

View File

@@ -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<MeasurementData.Response>() ?: 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<Void> = 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)
}
}

View File

@@ -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<Identity.Response>() ?: 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)
}
}

View File

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

View File

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

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%"
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:

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