mirror of
https://github.com/pcvolkmer/etl-processor.git
synced 2025-07-01 22:22:53 +00:00
Merge pull request #61 from CCC-MF/issue_55
Anzeige der letzten Verbindungsstatusänderung
This commit is contained in:
@ -21,7 +21,7 @@ package dev.dnpm.etl.processor.web
|
|||||||
|
|
||||||
import dev.dnpm.etl.processor.config.AppConfiguration
|
import dev.dnpm.etl.processor.config.AppConfiguration
|
||||||
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
import dev.dnpm.etl.processor.config.AppSecurityConfiguration
|
||||||
import dev.dnpm.etl.processor.monitoring.ConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.GPasConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
import dev.dnpm.etl.processor.monitoring.RestConnectionCheckService
|
||||||
import dev.dnpm.etl.processor.output.MtbFileSender
|
import dev.dnpm.etl.processor.output.MtbFileSender
|
||||||
import dev.dnpm.etl.processor.pseudonym.Generator
|
import dev.dnpm.etl.processor.pseudonym.Generator
|
||||||
@ -69,10 +69,10 @@ abstract class MockSink : Sinks.Many<Boolean>
|
|||||||
@MockBean(
|
@MockBean(
|
||||||
Generator::class,
|
Generator::class,
|
||||||
MtbFileSender::class,
|
MtbFileSender::class,
|
||||||
ConnectionCheckService::class,
|
|
||||||
RequestProcessor::class,
|
RequestProcessor::class,
|
||||||
TransformationService::class,
|
TransformationService::class,
|
||||||
TokenRepository::class,
|
TokenRepository::class,
|
||||||
|
GPasConnectionCheckService::class,
|
||||||
RestConnectionCheckService::class
|
RestConnectionCheckService::class
|
||||||
)
|
)
|
||||||
class ConfigControllerTest {
|
class ConfigControllerTest {
|
||||||
|
@ -26,22 +26,18 @@ import jakarta.annotation.PostConstruct
|
|||||||
import org.apache.kafka.clients.consumer.Consumer
|
import org.apache.kafka.clients.consumer.Consumer
|
||||||
import org.apache.kafka.common.errors.TimeoutException
|
import org.apache.kafka.common.errors.TimeoutException
|
||||||
import org.springframework.beans.factory.annotation.Qualifier
|
import org.springframework.beans.factory.annotation.Qualifier
|
||||||
import org.springframework.http.HttpEntity
|
import org.springframework.http.*
|
||||||
import org.springframework.http.HttpHeaders
|
|
||||||
import org.springframework.http.HttpMethod
|
|
||||||
import org.springframework.http.HttpStatus
|
|
||||||
import org.springframework.http.MediaType
|
|
||||||
import org.springframework.http.RequestEntity
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.web.client.RestTemplate
|
import org.springframework.web.client.RestTemplate
|
||||||
import org.springframework.web.util.UriComponentsBuilder
|
import org.springframework.web.util.UriComponentsBuilder
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import java.time.Instant
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
interface ConnectionCheckService {
|
interface ConnectionCheckService {
|
||||||
|
|
||||||
fun connectionAvailable(): Boolean
|
fun connectionAvailable(): ConnectionCheckResult
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,9 +47,27 @@ sealed class ConnectionCheckResult {
|
|||||||
|
|
||||||
abstract val available: Boolean
|
abstract val available: Boolean
|
||||||
|
|
||||||
data class KafkaConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
|
abstract val timestamp: Instant
|
||||||
data class RestConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
|
|
||||||
data class GPasConnectionCheckResult(override val available: Boolean) : ConnectionCheckResult()
|
abstract val lastChange: Instant
|
||||||
|
|
||||||
|
data class KafkaConnectionCheckResult(
|
||||||
|
override val available: Boolean,
|
||||||
|
override val timestamp: Instant,
|
||||||
|
override val lastChange: Instant
|
||||||
|
) : ConnectionCheckResult()
|
||||||
|
|
||||||
|
data class RestConnectionCheckResult(
|
||||||
|
override val available: Boolean,
|
||||||
|
override val timestamp: Instant,
|
||||||
|
override val lastChange: Instant
|
||||||
|
) : ConnectionCheckResult()
|
||||||
|
|
||||||
|
data class GPasConnectionCheckResult(
|
||||||
|
override val available: Boolean,
|
||||||
|
override val timestamp: Instant,
|
||||||
|
override val lastChange: Instant
|
||||||
|
) : ConnectionCheckResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
class KafkaConnectionCheckService(
|
class KafkaConnectionCheckService(
|
||||||
@ -62,25 +76,33 @@ class KafkaConnectionCheckService(
|
|||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
) : OutputConnectionCheckService {
|
) : OutputConnectionCheckService {
|
||||||
|
|
||||||
private var connectionAvailable: Boolean = false
|
private var result = ConnectionCheckResult.KafkaConnectionCheckResult(false, Instant.now(), Instant.now())
|
||||||
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun check() {
|
fun check() {
|
||||||
connectionAvailable = try {
|
result = try {
|
||||||
null != consumer.listTopics(5.seconds.toJavaDuration())
|
val available = null != consumer.listTopics(5.seconds.toJavaDuration())
|
||||||
|
ConnectionCheckResult.KafkaConnectionCheckResult(
|
||||||
|
available,
|
||||||
|
Instant.now(),
|
||||||
|
if (result.available == available) { result.lastChange } else { Instant.now() }
|
||||||
|
)
|
||||||
} catch (e: TimeoutException) {
|
} catch (e: TimeoutException) {
|
||||||
false
|
ConnectionCheckResult.KafkaConnectionCheckResult(
|
||||||
|
false,
|
||||||
|
Instant.now(),
|
||||||
|
if (!result.available) { result.lastChange } else { Instant.now() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
connectionCheckUpdateProducer.emitNext(
|
connectionCheckUpdateProducer.emitNext(
|
||||||
ConnectionCheckResult.KafkaConnectionCheckResult(connectionAvailable),
|
result,
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
Sinks.EmitFailureHandler.FAIL_FAST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionAvailable(): Boolean {
|
override fun connectionAvailable(): ConnectionCheckResult.KafkaConnectionCheckResult {
|
||||||
return this.connectionAvailable
|
return this.result
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -92,27 +114,37 @@ class RestConnectionCheckService(
|
|||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
) : OutputConnectionCheckService {
|
) : OutputConnectionCheckService {
|
||||||
|
|
||||||
private var connectionAvailable: Boolean = false
|
private var result = ConnectionCheckResult.RestConnectionCheckResult(false, Instant.now(), Instant.now())
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun check() {
|
fun check() {
|
||||||
connectionAvailable = try {
|
result = try {
|
||||||
restTemplate.getForEntity(
|
val available = restTemplate.getForEntity(
|
||||||
restTargetProperties.uri?.replace("/etl/api", "").toString(),
|
restTargetProperties.uri?.replace("/etl/api", "").toString(),
|
||||||
String::class.java
|
String::class.java
|
||||||
).statusCode == HttpStatus.OK
|
).statusCode == HttpStatus.OK
|
||||||
|
|
||||||
|
ConnectionCheckResult.RestConnectionCheckResult(
|
||||||
|
available,
|
||||||
|
Instant.now(),
|
||||||
|
if (result.available == available) { result.lastChange } else { Instant.now() }
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
ConnectionCheckResult.RestConnectionCheckResult(
|
||||||
|
false,
|
||||||
|
Instant.now(),
|
||||||
|
if (!result.available) { result.lastChange } else { Instant.now() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
connectionCheckUpdateProducer.emitNext(
|
connectionCheckUpdateProducer.emitNext(
|
||||||
ConnectionCheckResult.RestConnectionCheckResult(connectionAvailable),
|
result,
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
Sinks.EmitFailureHandler.FAIL_FAST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionAvailable(): Boolean {
|
override fun connectionAvailable(): ConnectionCheckResult.RestConnectionCheckResult {
|
||||||
return this.connectionAvailable
|
return this.result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,12 +155,12 @@ class GPasConnectionCheckService(
|
|||||||
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
private val connectionCheckUpdateProducer: Sinks.Many<ConnectionCheckResult>
|
||||||
) : ConnectionCheckService {
|
) : ConnectionCheckService {
|
||||||
|
|
||||||
private var connectionAvailable: Boolean = false
|
private var result = ConnectionCheckResult.GPasConnectionCheckResult(false, Instant.now(), Instant.now())
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
fun check() {
|
fun check() {
|
||||||
connectionAvailable = try {
|
result = try {
|
||||||
val uri = UriComponentsBuilder.fromUriString(
|
val uri = UriComponentsBuilder.fromUriString(
|
||||||
gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/\$pseudonymize").toString()
|
gPasConfigProperties.uri?.replace("/\$pseudonymizeAllowCreate", "/\$pseudonymize").toString()
|
||||||
)
|
)
|
||||||
@ -141,22 +173,33 @@ class GPasConnectionCheckService(
|
|||||||
if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) {
|
if (!gPasConfigProperties.username.isNullOrBlank() && !gPasConfigProperties.password.isNullOrBlank()) {
|
||||||
headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password)
|
headers.setBasicAuth(gPasConfigProperties.username, gPasConfigProperties.password)
|
||||||
}
|
}
|
||||||
restTemplate.exchange(
|
|
||||||
|
val available = restTemplate.exchange(
|
||||||
uri,
|
uri,
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
HttpEntity<Void>(headers),
|
HttpEntity<Void>(headers),
|
||||||
Void::class.java
|
Void::class.java
|
||||||
).statusCode == HttpStatus.OK
|
).statusCode == HttpStatus.OK
|
||||||
|
|
||||||
|
ConnectionCheckResult.GPasConnectionCheckResult(
|
||||||
|
available,
|
||||||
|
Instant.now(),
|
||||||
|
if (result.available == available) { result.lastChange } else { Instant.now() }
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
false
|
ConnectionCheckResult.GPasConnectionCheckResult(
|
||||||
|
false,
|
||||||
|
Instant.now(),
|
||||||
|
if (!result.available) { result.lastChange } else { Instant.now() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
connectionCheckUpdateProducer.emitNext(
|
connectionCheckUpdateProducer.emitNext(
|
||||||
ConnectionCheckResult.GPasConnectionCheckResult(connectionAvailable),
|
result,
|
||||||
Sinks.EmitFailureHandler.FAIL_FAST
|
Sinks.EmitFailureHandler.FAIL_FAST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun connectionAvailable(): Boolean {
|
override fun connectionAvailable(): ConnectionCheckResult.GPasConnectionCheckResult {
|
||||||
return this.connectionAvailable
|
return this.result
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -56,7 +56,7 @@ class ConfigController(
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
fun index(model: Model): String {
|
fun index(model: Model): String {
|
||||||
val outputConnectionAvailable =
|
val outputConnectionAvailable =
|
||||||
connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().first().connectionAvailable()
|
connectionCheckServices.filterIsInstance<OutputConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
||||||
|
|
||||||
val gPasConnectionAvailable =
|
val gPasConnectionAvailable =
|
||||||
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
connectionCheckServices.filterIsInstance<GPasConnectionCheckService>().firstOrNull()?.connectionAvailable()
|
||||||
|
@ -2,15 +2,20 @@
|
|||||||
<h2><span>🟦</span> gPAS nicht konfiguriert - Patienten-IDs werden intern anonymisiert</h2>
|
<h2><span>🟦</span> gPAS nicht konfiguriert - Patienten-IDs werden intern anonymisiert</h2>
|
||||||
</th:block>
|
</th:block>
|
||||||
<th:block th:if="${gPasConnectionAvailable != null}">
|
<th:block th:if="${gPasConnectionAvailable != null}">
|
||||||
<h2><span th:if="${gPasConnectionAvailable}">✅</span><span th:if="${not(gPasConnectionAvailable)}">⚡</span> Verbindung zu gPAS</h2>
|
<h2><span th:if="${gPasConnectionAvailable.available}">✅</span><span th:if="${not(gPasConnectionAvailable.available)}">⚡</span> Verbindung zu gPAS</h2>
|
||||||
<div>
|
<div>
|
||||||
Die Verbindung ist aktuell
|
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.timestamp)}"></time>
|
||||||
<strong th:if="${gPasConnectionAvailable}" style="color: green">verfügbar.</strong>
|
|
|
||||||
<strong th:if="${not(gPasConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
|
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(gPasConnectionAvailable.lastChange)}"></time>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Die Verbindung ist aktuell</span>
|
||||||
|
<strong th:if="${gPasConnectionAvailable.available}" style="color: green">verfügbar.</strong>
|
||||||
|
<strong th:if="${not(gPasConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="connection-display border">
|
<div class="connection-display border">
|
||||||
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
||||||
<span class="connection" th:classappend="${gPasConnectionAvailable ? 'available' : ''}"></span>
|
<span class="connection" th:classappend="${gPasConnectionAvailable.available ? 'available' : ''}"></span>
|
||||||
<img th:src="@{/server.png}" alt="gPAS" />
|
<img th:src="@{/server.png}" alt="gPAS" />
|
||||||
<span>ETL-Processor</span>
|
<span>ETL-Processor</span>
|
||||||
<span></span>
|
<span></span>
|
||||||
|
@ -1,16 +1,26 @@
|
|||||||
<h2><span th:if="${outputConnectionAvailable}">✅</span><span th:if="${not(outputConnectionAvailable)}">⚡</span> MTB-File Verbindung</h2>
|
<th:block th:if="${outputConnectionAvailable == null}">
|
||||||
<div>
|
<h2><span>🟦</span> Keine Ausgabenkonfiguration</h2>
|
||||||
Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
|
</th:block>
|
||||||
<strong th:if="${outputConnectionAvailable}" style="color: green">verfügbar.</strong>
|
<th:block th:if="${outputConnectionAvailable != null}">
|
||||||
<strong th:if="${not(outputConnectionAvailable)}" style="color: red">nicht verfügbar.</strong>
|
<h2><span th:if="${outputConnectionAvailable.available}">✅</span><span th:if="${not(outputConnectionAvailable.available)}">⚡</span> MTB-File Verbindung</h2>
|
||||||
</div>
|
<div>
|
||||||
<div class="connection-display border">
|
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(outputConnectionAvailable.timestamp)}"></time>
|
||||||
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
|
|
||||||
<span class="connection" th:classappend="${outputConnectionAvailable ? 'available' : ''}"></span>
|
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(outputConnectionAvailable.lastChange)}"></time>
|
||||||
<img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
|
</div>
|
||||||
<img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
|
<div>
|
||||||
<span>ETL-Processor</span>
|
Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
|
||||||
<span></span>
|
<strong th:if="${outputConnectionAvailable.available}" style="color: green">verfügbar.</strong>
|
||||||
<span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
|
<strong th:if="${not(outputConnectionAvailable.available)}" style="color: red">nicht verfügbar.</strong>
|
||||||
<span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
|
</div>
|
||||||
</div>
|
<div class="connection-display border">
|
||||||
|
<img th:src="@{/server.png}" alt="ETL-Processor" />
|
||||||
|
<span class="connection" th:classappend="${outputConnectionAvailable.available ? 'available' : ''}"></span>
|
||||||
|
<img th:if="${mtbFileSender.startsWith('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
|
||||||
|
<img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
|
||||||
|
<span>ETL-Processor</span>
|
||||||
|
<span></span>
|
||||||
|
<span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
|
||||||
|
<span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
|
||||||
|
</div>
|
||||||
|
</th:block>
|
Reference in New Issue
Block a user