1
0
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:
2024-03-26 10:06:27 +01:00
committed by GitHub
5 changed files with 114 additions and 56 deletions

View File

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

View File

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

View File

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

View File

@ -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> &nbsp;|&nbsp;
<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>

View File

@ -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>
</th:block>
<th:block th:if="${outputConnectionAvailable != null}">
<h2><span th:if="${outputConnectionAvailable.available}"></span><span th:if="${not(outputConnectionAvailable.available)}"></span> MTB-File Verbindung</h2>
<div>
Stand: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.timestamp)}" th:text="${#temporals.formatISO(outputConnectionAvailable.timestamp)}"></time>
&nbsp;|&nbsp;
Letzte Änderung: <time style="font-weight: bold" th:datetime="${#temporals.formatISO(outputConnectionAvailable.lastChange)}" th:text="${#temporals.formatISO(outputConnectionAvailable.lastChange)}"></time>
</div>
<div>
Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell Verbindung über <code>[[ ${mtbFileSender} ]]</code>. Die Verbindung ist aktuell
<strong th:if="${outputConnectionAvailable}" style="color: green">verfügbar.</strong> <strong th:if="${outputConnectionAvailable.available}" style="color: green">verfügbar.</strong>
<strong th:if="${not(outputConnectionAvailable)}" style="color: red">nicht verfügbar.</strong> <strong th:if="${not(outputConnectionAvailable.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="${outputConnectionAvailable ? 'available' : ''}"></span> <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('Rest')}" th:src="@{/server.png}" alt="bwHC-Backend" />
<img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" /> <img th:if="${mtbFileSender.startsWith('Kafka')}" th:src="@{/kafka.png}" alt="Kafka-Broker" />
<span>ETL-Processor</span> <span>ETL-Processor</span>
<span></span> <span></span>
<span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span> <span th:if="${mtbFileSender.startsWith('Rest')}">bwHC-Backend</span>
<span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span> <span th:if="${mtbFileSender.startsWith('Kafka')}">Kafka-Broker</span>
</div> </div>
</th:block>