chore: initial commit
Signed-off-by: Alan Brault <alan.brault@visus.io>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.visus.demos.kotlinapi.domain.model
|
||||
|
||||
data class ComponentHealth(
|
||||
val name: String,
|
||||
val status: ComponentStatus,
|
||||
val message: String? = null,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.visus.demos.kotlinapi.domain.model
|
||||
|
||||
enum class ComponentStatus {
|
||||
UP,
|
||||
DOWN,
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.visus.demos.kotlinapi.domain.model
|
||||
|
||||
enum class Status {
|
||||
UP,
|
||||
DOWN,
|
||||
DEGRADED,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.visus.demos.kotlinapi.domain.service
|
||||
|
||||
import io.visus.demos.kotlinapi.domain.model.ComponentHealth
|
||||
|
||||
interface HealthService {
|
||||
fun check(): ComponentHealth
|
||||
}
|
||||
@@ -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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user