1
0

chore: initial commit

Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
2026-03-01 07:57:28 -05:00
commit 4c81006729
24 changed files with 825 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package io.visus.demos.kotlinapi
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class KotlinApiApplication
fun main(args: Array<String>) {
runApplication<KotlinApiApplication>(*args)
}

View File

@@ -0,0 +1,45 @@
package io.visus.demos.kotlinapi.api.dto
import io.swagger.v3.oas.annotations.media.Schema
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
import io.visus.demos.kotlinapi.domain.model.HealthStatus
@Schema(description = "Health check response")
data class HealthResponse(
@Schema(description = "Current status of the API", example = "UP")
val status: String,
@Schema(description = "Timestamp of the health check", example = "2024-01-01T12:00:00Z")
val timestamp: String,
@Schema(description = "Status of individual components")
val components: Map<String, ComponentHealthDto>? = null,
) {
companion object {
fun from(healthStatus: HealthStatus): HealthResponse =
HealthResponse(
status = healthStatus.status.name,
timestamp = healthStatus.timestamp.toString(),
components =
if (healthStatus.components.isNotEmpty()) {
healthStatus.components.mapValues { ComponentHealthDto.from(it.value) }
} else {
null
},
)
}
}
@Schema(description = "Health status of an individual component")
data class ComponentHealthDto(
@Schema(description = "Component status", example = "UP")
val status: String,
@Schema(description = "Optional message with additional details", example = "MongoDB connection is active")
val message: String? = null,
) {
companion object {
fun from(componentHealth: ComponentHealth): ComponentHealthDto =
ComponentHealthDto(
status = componentHealth.status.name,
message = componentHealth.message,
)
}
}

View File

@@ -0,0 +1,34 @@
package io.visus.demos.kotlinapi.config
import com.mongodb.ConnectionString
import com.mongodb.MongoClientSettings
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.concurrent.TimeUnit
@Configuration
class MongoConfig {
@Bean
fun mongoClient(
@Value("\${spring.mongodb.uri}") uri: String,
): MongoClient {
val connectionString = ConnectionString(uri)
val settings =
MongoClientSettings
.builder()
.applyConnectionString(connectionString)
.applyToSocketSettings { builder ->
builder
.connectTimeout(2, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
}.applyToClusterSettings { builder ->
builder.serverSelectionTimeout(2, TimeUnit.SECONDS)
}.build()
return MongoClients.create(settings)
}
}

View File

@@ -0,0 +1,19 @@
package io.visus.demos.kotlinapi.config
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class OpenApiConfig {
@Bean
fun customOpenAPI(): OpenAPI =
OpenAPI()
.info(
Info()
.title("Kotlin API")
.version("0.0.1-SNAPSHOT")
.description("API documentation for Kotlin Spring Boot application"),
)
}

View File

@@ -0,0 +1,27 @@
package io.visus.demos.kotlinapi.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http
.authorizeHttpRequests { authorize ->
authorize
.requestMatchers("/health")
.permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest()
.authenticated()
}
return http.build()
}
}

View File

@@ -0,0 +1,63 @@
package io.visus.demos.kotlinapi.controller
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.tags.Tag
import io.visus.demos.kotlinapi.api.dto.HealthResponse
import io.visus.demos.kotlinapi.domain.model.HealthStatus
import io.visus.demos.kotlinapi.domain.model.Status
import io.visus.demos.kotlinapi.domain.service.HealthService
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@Tag(name = "Health", description = "Health check endpoints")
class HealthController(
private val healthCheckServices: List<HealthService>,
) {
@GetMapping("/health", produces = [MediaType.APPLICATION_JSON_VALUE])
@Operation(
summary = "Health check",
description = "Returns the health status of the API",
responses = [
ApiResponse(
responseCode = "200",
description = "API is healthy",
content = [
Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = HealthResponse::class),
),
],
),
ApiResponse(
responseCode = "503",
description = "API is down or degraded",
content = [
Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = HealthResponse::class),
),
],
),
],
)
fun health(): ResponseEntity<HealthResponse> {
val componentHealths = healthCheckServices.map { it.check() }
val healthStatus = HealthStatus.fromComponents(componentHealths)
val response = HealthResponse.from(healthStatus)
val httpStatus =
when (healthStatus.status) {
Status.UP -> HttpStatus.OK
Status.DOWN, Status.DEGRADED -> HttpStatus.SERVICE_UNAVAILABLE
}
return ResponseEntity.status(httpStatus).body(response)
}
}

View File

@@ -0,0 +1,7 @@
package io.visus.demos.kotlinapi.domain.model
data class ComponentHealth(
val name: String,
val status: ComponentStatus,
val message: String? = null,
)

View File

@@ -0,0 +1,6 @@
package io.visus.demos.kotlinapi.domain.model
enum class ComponentStatus {
UP,
DOWN,
}

View File

@@ -0,0 +1,27 @@
package io.visus.demos.kotlinapi.domain.model
import java.time.Instant
data class HealthStatus(
val status: Status,
val timestamp: Instant,
val components: Map<String, ComponentHealth> = emptyMap(),
) {
companion object {
fun fromComponents(components: List<ComponentHealth>): HealthStatus {
val componentMap = components.associateBy { it.name }
val status =
when {
components.all { it.status == ComponentStatus.UP } -> Status.UP
components.all { it.status == ComponentStatus.DOWN } -> Status.DOWN
else -> Status.DEGRADED
}
return HealthStatus(
status = status,
timestamp = Instant.now(),
components = componentMap,
)
}
}
}

View File

@@ -0,0 +1,7 @@
package io.visus.demos.kotlinapi.domain.model
enum class Status {
UP,
DOWN,
DEGRADED,
}

View File

@@ -0,0 +1,7 @@
package io.visus.demos.kotlinapi.domain.service
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
interface HealthService {
fun check(): ComponentHealth
}

View File

@@ -0,0 +1,31 @@
package io.visus.demos.kotlinapi.infrastructure.health
import com.mongodb.client.MongoClient
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
import io.visus.demos.kotlinapi.domain.model.ComponentStatus
import io.visus.demos.kotlinapi.domain.service.HealthService
import org.bson.Document
import org.springframework.stereotype.Service
@Service
class MongoHealthService(
private val mongoClient: MongoClient,
) : HealthService {
override fun check(): ComponentHealth =
try {
val command = Document("ping", 1)
mongoClient.getDatabase("admin").runCommand(command)
ComponentHealth(
name = "mongodb",
status = ComponentStatus.UP,
message = "MongoDB connection is active",
)
} catch (e: Exception) {
ComponentHealth(
name = "mongodb",
status = ComponentStatus.DOWN,
message = "MongoDB connection failed: ${e.message}",
)
}
}