Sett opp Prometheus-metrikker, OpenTelemetry-tracing og health check-endepunkter for Nais-applikasjoner
This skill provides patterns for setting up observability in Nais applications.
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
fun Application.configureHealthEndpoints(
dataSource: HikariDataSource,
kafkaProducer: KafkaProducer<String, String>
) {
routing {
get("/isalive") {
call.respondText("Alive", ContentType.Text.Plain)
}
get("/isready") {
val databaseHealthy = checkDatabase(dataSource)
val kafkaHealthy = checkKafka(kafkaProducer)
if (databaseHealthy && kafkaHealthy) {
call.respondText("Ready", ContentType.Text.Plain)
} else {
call.respondText(
"Not ready",
ContentType.Text.Plain,
HttpStatusCode.ServiceUnavailable
)
}
}
}
}
fun checkDatabase(dataSource: HikariDataSource): Boolean {
return try {
dataSource.connection.use { it.isValid(1) }
} catch (e: Exception) {
false
}
}
fun checkKafka(producer: KafkaProducer<String, String>): Boolean {
return try {
producer.partitionsFor("health-check-topic").isNotEmpty()
} catch (e: Exception) {
false
}
}
import io.micrometer.core.instrument.Clock
import io.micrometer.core.instrument.binder.jvm.*
import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import io.prometheus.client.CollectorRegistry
import io.ktor.server.metrics.micrometer.*
import io.ktor.server.response.*
import io.ktor.http.*
val meterRegistry = PrometheusMeterRegistry(
PrometheusConfig.DEFAULT,
CollectorRegistry.defaultRegistry,
Clock.SYSTEM
)
fun Application.configureMetrics() {
install(MicrometerMetrics) {
registry = meterRegistry
// Production pattern from navikt/ao-oppfolgingskontor
meterBinders = listOf(
JvmMemoryMetrics(), // Heap, non-heap memory
JvmGcMetrics(), // Garbage collection
ProcessorMetrics(), // CPU usage
UptimeMetrics() // Application uptime
)
}
routing {
get("/metrics") {
call.respondText(
meterRegistry.scrape(),
ContentType.parse("text/plain; version=0.0.4")
)
}
}
}
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.Timer
class UserService(private val meterRegistry: PrometheusMeterRegistry) {
private val userCreatedCounter = Counter.builder("users_created_total")
.description("Total users created")
.register(meterRegistry)
private val userCreationTimer = Timer.builder("user_creation_duration_seconds")
.description("User creation duration")
.register(meterRegistry)
fun createUser(user: User) {
userCreationTimer.record {
repository.save(user)
}
userCreatedCounter.increment()
}
}
Nais enables OpenTelemetry auto-instrumentation by default. For manual spans:
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.StatusCode
val tracer = GlobalOpenTelemetry.getTracer("my-app")
fun processPayment(paymentId: String) {
val span = tracer.spanBuilder("processPayment")
.setAttribute("payment.id", paymentId)
.startSpan()
try {
// Business logic
val payment = repository.findPayment(paymentId)
span.setAttribute("payment.amount", payment.amount)
processPaymentInternal(payment)
span.setStatus(StatusCode.OK)
} catch (e: Exception) {
span.setStatus(StatusCode.ERROR, "Payment processing failed")
span.recordException(e)
throw e
} finally {
span.end()
}
}
import mu.KotlinLogging
import net.logstash.logback.argument.StructuredArguments.kv
private val logger = KotlinLogging.logger {}
fun processOrder(orderId: String) {
logger.info(
"Processing order",
kv("order_id", orderId),
kv("timestamp", LocalDateTime.now())
)
try {
orderService.process(orderId)
logger.info(
"Order processed successfully",
kv("order_id", orderId)
)
} catch (e: Exception) {
logger.error(
"Order processing failed",
kv("order_id", orderId),
kv("error", e.message),
e
)
throw e
}
}
apiVersion: nais.io/v1alpha1