Versuche
This commit is contained in:
@@ -1,131 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.8
|
||||
|
||||
# ===================================================================
|
||||
# Multi-stage Dockerfile for Meldestelle Entries Service
|
||||
# ===================================================================
|
||||
|
||||
# === CENTRALIZED BUILD ARGUMENTS ===
|
||||
ARG GRADLE_VERSION
|
||||
ARG JAVA_VERSION
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
|
||||
FROM gradle:${GRADLE_VERSION}-jdk${JAVA_VERSION}-alpine AS builder
|
||||
|
||||
LABEL stage=builder \
|
||||
service=entries-service \
|
||||
maintainer="Meldestelle Development Team" \
|
||||
version="${VERSION}" \
|
||||
build.date="${BUILD_DATE}"
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
ENV GRADLE_OPTS="-Dorg.gradle.caching=true \
|
||||
-Dorg.gradle.daemon=false \
|
||||
-Dorg.gradle.parallel=true \
|
||||
-Dorg.gradle.workers.max=2 \
|
||||
-Dorg.gradle.jvmargs=-Xmx2g \
|
||||
-XX:+UseParallelGC \
|
||||
-XX:MaxMetaspaceSize=512m"
|
||||
|
||||
ENV GRADLE_USER_HOME=/home/gradle/.gradle
|
||||
|
||||
COPY gradlew gradlew.bat gradle.properties settings.gradle.kts ./
|
||||
COPY gradle/ gradle/
|
||||
RUN chmod +x gradlew
|
||||
COPY platform/ platform/
|
||||
COPY frontend/ frontend/
|
||||
COPY core/ core/
|
||||
COPY backend/ backend/
|
||||
COPY docs/ docs/
|
||||
COPY build.gradle.kts ./
|
||||
|
||||
# Copy entries modules
|
||||
COPY backend/services/entries/entries-service/ backend/services/entries/entries-service/
|
||||
|
||||
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||
./gradlew :backend:services:entries:entries-service:dependencies --no-daemon --info
|
||||
|
||||
RUN --mount=type=cache,target=/home/gradle/.gradle/caches \
|
||||
--mount=type=cache,target=/home/gradle/.gradle/wrapper \
|
||||
./gradlew :backend:services:entries:entries-service:bootJar --no-daemon --info
|
||||
|
||||
# ===================================================================
|
||||
# Runtime stage
|
||||
# ===================================================================
|
||||
FROM eclipse-temurin:${JAVA_VERSION}-jre-alpine AS runtime
|
||||
|
||||
ARG BUILD_DATE
|
||||
ARG VERSION
|
||||
ARG JAVA_VERSION
|
||||
|
||||
ENV JAVA_VERSION=${JAVA_VERSION} \
|
||||
VERSION=${VERSION} \
|
||||
BUILD_DATE=${BUILD_DATE}
|
||||
|
||||
LABEL service="entries-service" \
|
||||
version="${VERSION}" \
|
||||
description="Microservice for Entries Management" \
|
||||
maintainer="Meldestelle Development Team" \
|
||||
java.version="${JAVA_VERSION}" \
|
||||
build.date="${BUILD_DATE}"
|
||||
|
||||
ARG APP_USER=appuser
|
||||
ARG APP_GROUP=appgroup
|
||||
ARG APP_UID=1001
|
||||
ARG APP_GID=1001
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk update && \
|
||||
apk upgrade && \
|
||||
apk add --no-cache curl tzdata tini && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
addgroup -g ${APP_GID} -S ${APP_GROUP} && \
|
||||
adduser -u ${APP_UID} -S ${APP_USER} -G ${APP_GROUP} -h /app -s /bin/sh && \
|
||||
mkdir -p /app/logs /app/tmp /app/config && \
|
||||
chown -R ${APP_USER}:${APP_GROUP} /app && \
|
||||
chmod -R 750 /app
|
||||
|
||||
COPY --from=builder --chown=${APP_USER}:${APP_GROUP} \
|
||||
/workspace/backend/services/entries/entries-service/build/libs/*.jar app.jar
|
||||
|
||||
USER ${APP_USER}
|
||||
|
||||
EXPOSE 8083 5005
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=3s --start-period=40s --retries=3 \
|
||||
CMD curl -fsS --max-time 2 http://localhost:8083/actuator/health/readiness || exit 1
|
||||
|
||||
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \
|
||||
-XX:+UseG1GC \
|
||||
-XX:+UseStringDeduplication \
|
||||
-XX:+UseContainerSupport \
|
||||
-XX:G1HeapRegionSize=16m \
|
||||
-XX:G1ReservePercent=25 \
|
||||
-XX:InitiatingHeapOccupancyPercent=30 \
|
||||
-XX:+AlwaysPreTouch \
|
||||
-XX:+DisableExplicitGC \
|
||||
-Djava.security.egd=file:/dev/./urandom \
|
||||
-Djava.awt.headless=true \
|
||||
-Dfile.encoding=UTF-8 \
|
||||
-Duser.timezone=Europe/Vienna \
|
||||
-Dspring.backgroundpreinitializer.ignore=true \
|
||||
-Dmanagement.endpoints.web.exposure.include=health,info,metrics,prometheus \
|
||||
-Dmanagement.endpoint.health.show-details=always \
|
||||
-Dmanagement.prometheus.metrics.export.enabled=true"
|
||||
|
||||
ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS
|
||||
ENV SERVER_PORT=8083
|
||||
ENV LOGGING_LEVEL_ROOT=INFO
|
||||
|
||||
ENTRYPOINT ["tini", "--", "sh", "-c", "\
|
||||
echo 'Starting Entries Service with Java ${JAVA_VERSION}...'; \
|
||||
echo 'Service port: ${SERVER_PORT}'; \
|
||||
if [ \"${DEBUG:-false}\" = \"true\" ]; then \
|
||||
echo 'DEBUG mode enabled'; \
|
||||
exec java ${JAVA_OPTS} -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar; \
|
||||
else \
|
||||
exec java ${JAVA_OPTS} -jar app.jar; \
|
||||
fi"]
|
||||
@@ -13,6 +13,7 @@ springBoot {
|
||||
dependencies {
|
||||
implementation(platform(projects.platform.platformBom))
|
||||
implementation(projects.platform.platformDependencies)
|
||||
implementation(projects.backend.services.entries.entriesApi)
|
||||
implementation(projects.backend.infrastructure.monitoring.monitoringClient)
|
||||
|
||||
implementation(libs.bundles.spring.boot.service.complete)
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package at.mocode.entries.service
|
||||
|
||||
import at.mocode.entries.api.EnhancedEntriesResponse
|
||||
import at.mocode.entries.api.EntriesApi
|
||||
import at.mocode.entries.api.HealthResponse
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@CrossOrigin(
|
||||
origins = ["http://localhost:8080", "http://localhost:8083", "http://localhost:4000"],
|
||||
methods = [RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE, RequestMethod.OPTIONS],
|
||||
allowedHeaders = ["*"],
|
||||
allowCredentials = "true"
|
||||
)
|
||||
class EntriesController(
|
||||
private val entriesService: EntriesServiceCircuitBreaker
|
||||
) : EntriesApi {
|
||||
|
||||
// Contract endpoints
|
||||
@GetMapping("/entries/enhanced")
|
||||
suspend fun enhancedEntries(
|
||||
@RequestParam(required = false, defaultValue = "false") simulate: Boolean
|
||||
): EnhancedEntriesResponse = entriesService.entries(simulate)
|
||||
|
||||
@GetMapping("/entries/health")
|
||||
override suspend fun healthCheck(): HealthResponse = entriesService.healthCheck()
|
||||
}
|
||||
+23
-15
@@ -2,26 +2,34 @@ package at.mocode.entries.service
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import org.springframework.boot.runApplication
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity
|
||||
|
||||
@SpringBootApplication
|
||||
class EntriesServiceApplication
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
runApplication<EntriesServiceApplication>(*args)
|
||||
}
|
||||
|
||||
@RestController
|
||||
class EntriesController {
|
||||
@GetMapping("/")
|
||||
fun health(): String = "Entries Service is running"
|
||||
@SpringBootApplication
|
||||
@EnableAspectJAutoProxy
|
||||
class EntriesServiceApplication {
|
||||
|
||||
@PostMapping("/entries/conflict-demo")
|
||||
fun conflictDemo(): ResponseEntity<String> {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body("Conflict detected (Demo)")
|
||||
@Bean
|
||||
fun corsConfigurer(): WebMvcConfigurer {
|
||||
return object : WebMvcConfigurer {
|
||||
override fun addCorsMappings(registry: CorsRegistry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("http://localhost:*")
|
||||
.allowedOrigins("http://localhost:8080",
|
||||
"http://localhost:8083",
|
||||
"http://localhost:4000"
|
||||
)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package at.mocode.entries.service
|
||||
|
||||
import at.mocode.entries.api.EnhancedEntriesResponse
|
||||
import at.mocode.entries.api.HealthResponse
|
||||
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Service demonstrating a Circuit Breaker pattern with Resilience
|
||||
*
|
||||
* This service simulates potential failures and uses circuit breaker
|
||||
* to handle service degradation gracefully with fallback responses.
|
||||
*/
|
||||
@Service
|
||||
class EntriesServiceCircuitBreaker {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(EntriesServiceCircuitBreaker::class.java)
|
||||
|
||||
companion object {
|
||||
const val ENTRIES_CIRCUIT_BREAKER = "entriesCircuitBreaker"
|
||||
private val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME //.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary entries method with circuit breaker protection returning DTO directly
|
||||
*
|
||||
* @param simulateFailure - if true, randomly throws exceptions to test circuit breaker
|
||||
*/
|
||||
@CircuitBreaker(name = ENTRIES_CIRCUIT_BREAKER, fallbackMethod = "fallbackEntries")
|
||||
fun entries(simulateFailure: Boolean = false): EnhancedEntriesResponse {
|
||||
val start = System.nanoTime()
|
||||
logger.info("Executing entries service call...")
|
||||
|
||||
if (simulateFailure && Random.nextDouble() < 0.6) {
|
||||
logger.warn("Simulating service failure for circuit breaker testing")
|
||||
throw RuntimeException("Simulated service failure")
|
||||
}
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
logger.info("Entries service call successful")
|
||||
|
||||
return EnhancedEntriesResponse(
|
||||
status = "entries",
|
||||
timestamp = currentTime,
|
||||
service = "entries-service",
|
||||
circuitBreakerState = "CLOSED",
|
||||
responseTime = elapsedMs
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback method called when circuit breaker is OPEN
|
||||
*
|
||||
* @param simulateFailure - original parameter (ignored in fallback)
|
||||
* @param exception - the exception that triggered the fallback
|
||||
*/
|
||||
fun fallbackEntries(simulateFailure: Boolean = false, exception: Exception): EnhancedEntriesResponse {
|
||||
val start = System.nanoTime()
|
||||
// Die volle Exception nur loggen, nicht an den Client weitergeben.
|
||||
logger.warn("Circuit breaker fallback triggered due to: {}", exception.toString())
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
|
||||
val elapsedMs = (System.nanoTime() - start) / 1_000_000
|
||||
|
||||
return EnhancedEntriesResponse(
|
||||
status = "fallback",
|
||||
timestamp = currentTime,
|
||||
service = "entries-service-fallback",
|
||||
circuitBreakerState = "OPEN",
|
||||
responseTime = elapsedMs
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Health check method with circuit breaker protection returning DTO directly
|
||||
*/
|
||||
@CircuitBreaker(name = ENTRIES_CIRCUIT_BREAKER, fallbackMethod = "fallbackHealth")
|
||||
fun healthCheck(): HealthResponse {
|
||||
logger.info("Executing health check...")
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
|
||||
return HealthResponse(
|
||||
status = "entries",
|
||||
timestamp = currentTime,
|
||||
service = "entries-service",
|
||||
healthy = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for health check returning DTO
|
||||
*/
|
||||
fun fallbackHealth(exception: Exception): HealthResponse {
|
||||
logger.warn("Health check fallback triggered: {}", exception.message)
|
||||
|
||||
val currentTime = LocalDateTime.now().atOffset(ZoneOffset.UTC).format(formatter)
|
||||
return HealthResponse(
|
||||
status = "down",
|
||||
timestamp = currentTime,
|
||||
service = "entries-service",
|
||||
healthy = false
|
||||
)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package at.mocode.entries.service.config
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
/**
|
||||
* Security configuration for the Entries Service.
|
||||
* Enables method-level security for fine-grained authorization control.
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(prePostEnabled = true)
|
||||
class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
return http
|
||||
.csrf { it.disable() }
|
||||
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
|
||||
.authorizeHttpRequests { auth ->
|
||||
auth
|
||||
// Allow health check endpoints
|
||||
.requestMatchers("/actuator/**", "/health/**").permitAll()
|
||||
// Allow ping endpoints for monitoring (these are typically public)
|
||||
.requestMatchers("/entries/**").permitAll()
|
||||
// All other endpoints require authentication (handled by method-level security)
|
||||
.anyRequest().authenticated()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
spring:
|
||||
application:
|
||||
name: entries-service
|
||||
cloud:
|
||||
consul:
|
||||
host: ${CONSUL_HOST:localhost}
|
||||
port: ${CONSUL_PORT:8500}
|
||||
discovery:
|
||||
enabled: true
|
||||
register: true
|
||||
health-check-path: /actuator/health
|
||||
health-check-interval: 10s
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:${ENTRIES_SERVICE_PORT:8083}}
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics,prometheus,circuitbreakers
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
probes:
|
||||
enabled: true
|
||||
tracing:
|
||||
enabled: ${TRACING_ENABLED:false}
|
||||
sampling:
|
||||
probability: ${TRACING_SAMPLING_PROBABILITY:0.1}
|
||||
zipkin:
|
||||
tracing:
|
||||
endpoint: ${ZIPKIN_TRACING_ENDPOINT:http://localhost:9411/api/v2/spans}
|
||||
connect-timeout: 1s
|
||||
read-timeout: 5s
|
||||
# Resilience4j Circuit Breaker Configuration
|
||||
resilience4j:
|
||||
circuitbreaker:
|
||||
configs:
|
||||
default:
|
||||
# Circuit breaker opens when the failure rate exceeds 50%
|
||||
failure-rate-threshold: 50
|
||||
# Minimum number of calls to calculate the failure rate
|
||||
minimum-number-of-calls: 5
|
||||
# Time to wait before transitioning from OPEN to HALF_OPEN
|
||||
wait-duration-in-open-state: 10s
|
||||
# Number of calls in HALF_OPEN state before deciding to close/open
|
||||
permitted-number-of-calls-in-half-open-state: 3
|
||||
# Sliding window size for calculating failure rate
|
||||
sliding-window-size: 10
|
||||
# Type of sliding window (COUNT_BASED or TIME_BASED)
|
||||
sliding-window-type: COUNT_BASED
|
||||
# Record exceptions that should be considered as failures
|
||||
record-exceptions:
|
||||
- java.lang.Exception
|
||||
# Ignore certain exceptions (don't count as failures)
|
||||
ignore-exceptions:
|
||||
- java.lang.IllegalArgumentException
|
||||
instances:
|
||||
entriesCircuitBreaker:
|
||||
# Use default configuration
|
||||
base-config: default
|
||||
# Override specific settings for this instance if needed
|
||||
failure-rate-threshold: 60
|
||||
minimum-number-of-calls: 4
|
||||
wait-duration-in-open-state: 5s
|
||||
|
||||
# Metrics configuration removed to avoid property resolution warnings
|
||||
# Use micrometer and actuator endpoints for metrics instead
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<property name="LOG_PATTERN" value="%d{ISO8601} %-5level [%X{traceId:-}:%X{spanId:-}] %logger{36} - %msg%n"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${LOG_PATTERN}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework" level="INFO"/>
|
||||
<logger name="org.springframework.web" level="INFO"/>
|
||||
<logger name="org.springframework.boot.actuate" level="INFO"/>
|
||||
<logger name="reactor.netty" level="WARN"/>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user