diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3bf3a7e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI Pipeline + +on: + push: + branches: + - develop + - main + tags: + - 'v*' + + +env: + DOCKERHUB_USERNAME: threeamigoscoding + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '25' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0b6dd653ba04f4f93bf581ec31e66cbd7dcb644d + + - name: Build with Gradle + run: ./gradlew clean build + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: build-artifact + path: build/libs/*.jar + retention-days: 1 + + publish: + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: build-artifact + path: build/libs + + - name: Generate version tag + id: version + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + VERSION_TAG=${GITHUB_REF#refs/tags/v} + elif [[ $GITHUB_REF == refs/heads/main ]]; then + SHORT_SHA=$(git rev-parse --short HEAD) + VERSION_TAG="main-${SHORT_SHA}" + else + SHORT_SHA=$(git rev-parse --short HEAD) + VERSION_TAG="develop-${SHORT_SHA}" + fi + echo "tag=$VERSION_TAG" >> $GITHUB_OUTPUT + echo "Generated version tag: $VERSION_TAG" + + - name: Build and push Docker image with Kaniko + env: + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + mkdir -p /tmp/kaniko/.docker + echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$(echo -n ${{ env.DOCKERHUB_USERNAME }}:${DOCKERHUB_TOKEN} | base64)\"}}}" > /tmp/kaniko/.docker/config.json + docker run \ + -v ${{ github.workspace }}:/workspace \ + -v /tmp/kaniko/.docker:/kaniko/.docker \ + gcr.io/kaniko-project/executor:latest \ + --context=/workspace \ + --dockerfile=/workspace/Dockerfile \ + --destination=${{ env.DOCKERHUB_USERNAME }}/devoops-gateway-service:${{ steps.version.outputs.tag }} \ + --destination=${{ env.DOCKERHUB_USERNAME }}/devoops-gateway-service:latest \ + --cache=true \ + --cache-repo=${{ env.DOCKERHUB_USERNAME }}/devoops-gateway-service-cache \ No newline at end of file diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..e6f3777 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,27 @@ +name: PR Check + +on: + pull_request: + branches: + - develop + - main + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '25' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Build with Gradle + run: ./gradlew clean build \ No newline at end of file diff --git a/.gitignore b/.gitignore index df0b53a..4a82c18 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ out/ .vscode/ ### Mac ### -.DS_Store \ No newline at end of file +.DS_Store + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e7440a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM eclipse-temurin:25-jre-alpine + +WORKDIR /app + +COPY build/libs/*SNAPSHOT.jar app.jar + +RUN addgroup -S spring && adduser -S spring -G spring +USER spring:spring + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 4686e9e..fb34cb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,10 +21,22 @@ repositories { extra["springCloudVersion"] = "2025.1.0" dependencies { - implementation("org.springframework.boot:spring-boot-starter-webmvc") - implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc") - testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation("org.springframework.boot:spring-boot-starter-webmvc") + implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc") + implementation("io.jsonwebtoken:jjwt-api:0.12.6") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6") + implementation("net.logstash.logback:logstash-logback-encoder:8.0") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("io.micrometer:micrometer-registry-prometheus") + //zipkin(tracing) + implementation("org.springframework.boot:spring-boot-micrometer-tracing-brave") + implementation("org.springframework.boot:spring-boot-starter-zipkin") + implementation("io.micrometer:micrometer-tracing-bridge-brave") + implementation("io.zipkin.reporter2:zipkin-reporter-brave") + developmentOnly("org.springframework.boot:spring-boot-devtools") + testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } dependencyManagement { diff --git a/environment/.local.env b/environment/.local.env new file mode 100644 index 0000000..e00a233 --- /dev/null +++ b/environment/.local.env @@ -0,0 +1,11 @@ +SERVER_PORT=8080 +USER_SERVICE_URL=http://devoops-user-service:8080 +ACCOMMODATION_SERVICE_URL=http://devoops-accommodation-service:8080 +NOTIFICATION_SERVICE_URL=http://devoops-notification-service:8080 +RATING_SERVICE_URL=http://devoops-rating-service:8080 +RESERVATION_SERVICE_URL=http://devoops-reservation-service:8080 +SEARCH_SERVICE_URL=http://devoops-search-service:8080 +LOGSTASH_HOST=logstash:5000 +ZIPKIN_HOST=zipkin +ZIPKIN_PORT=9411 +JWT_SECRET=dGhpcyBpcyBhIHZlcnkgc2VjdXJlIGtleSBmb3IgSFMyNTYgdGhhdCBpcyBhdCBsZWFzdCAyNTYgYml0cyBsb25n diff --git a/environment/helm/values.yaml b/environment/helm/values.yaml new file mode 100644 index 0000000..e454f94 --- /dev/null +++ b/environment/helm/values.yaml @@ -0,0 +1,54 @@ +fullnameOverride: devoops-gateway-service +replicaCount: 1 + +image: + registry: docker.io + repository: threeamigoscoding/devoops-gateway-service + tag: "latest" + pullPolicy: IfNotPresent + +service: + type: ClusterIP + httpPort: 8080 + grpc: + enabled: false + +ingress: + enabled: true + className: nginx + host: devoops.local + path: / + pathType: Prefix + annotations: {} + +resources: + requests: + memory: 256Mi + cpu: 250m + limits: + memory: 512Mi + cpu: 1000m + +health: + path: /actuator/health + periodSeconds: 10 + failureThreshold: 3 + startup: + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 60 + +configData: + JAVA_TOOL_OPTIONS: "-XX:+UseSerialGC -Xms128m -Xmx384m -XX:ActiveProcessorCount=1" + SERVER_PORT: "8080" + USER_SERVICE_URL: "http://devoops-user-service:8080" + ACCOMMODATION_SERVICE_URL: "http://devoops-accommodation-service:8080" + NOTIFICATION_SERVICE_URL: "http://devoops-notification-service:8080" + RATING_SERVICE_URL: "http://devoops-rating-service:8080" + RESERVATION_SERVICE_URL: "http://devoops-reservation-service:8080" + LOGSTASH_HOST: "devoops-logstash:5000" + ZIPKIN_HOST: "devoops-jaeger" + ZIPKIN_PORT: "9411" + +secretData: + JWT_SECRET: "dGhpcyBpcyBhIHZlcnkgc2VjdXJlIGtleSBmb3IgSFMyNTYgdGhhdCBpcyBhdCBsZWFzdCAyNTYgYml0cyBsb25n" diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/devoops/gateway/config/MetricsConfig.java b/src/main/java/com/devoops/gateway/config/MetricsConfig.java new file mode 100644 index 0000000..b31b504 --- /dev/null +++ b/src/main/java/com/devoops/gateway/config/MetricsConfig.java @@ -0,0 +1,45 @@ +package com.devoops.gateway.config; + +import io.micrometer.common.KeyValue; +import io.micrometer.common.KeyValues; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.server.observation.ServerRequestObservationContext; +import org.springframework.http.server.observation.ServerRequestObservationConvention; + +import java.util.stream.StreamSupport; + +@Configuration +public class MetricsConfig { + + @Bean + public ServerRequestObservationConvention serverRequestObservationConvention() { + return new DefaultServerRequestObservationConvention() { + @Override + @NonNull + public KeyValues getLowCardinalityKeyValues(@NonNull ServerRequestObservationContext context) { + KeyValues keyValues = super.getLowCardinalityKeyValues(context); + + HttpServletResponse response = context.getResponse(); + if (response != null && response.getStatus() == 404) { + HttpServletRequest request = context.getCarrier(); + assert request != null; + String originalUri = request.getRequestURI(); + + KeyValues filtered = KeyValues.of( + StreamSupport.stream(keyValues.spliterator(), false) + .filter(kv -> !"uri".equals(kv.getKey())) + .toArray(KeyValue[]::new) + ); + keyValues = filtered.and(KeyValue.of("uri", originalUri)); + } + + return keyValues; + } + }; + } +} diff --git a/src/main/java/com/devoops/gateway/filter/JwtAuthenticationFilter.java b/src/main/java/com/devoops/gateway/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..8623229 --- /dev/null +++ b/src/main/java/com/devoops/gateway/filter/JwtAuthenticationFilter.java @@ -0,0 +1,150 @@ +package com.devoops.gateway.filter; + +import com.devoops.gateway.service.JwtService; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.*; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE + 10) +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private record PublicEndpoint(String pattern, HttpMethod... methods) { + boolean matchesMethod(String method) { + if (methods.length == 0) { + return true; // No method restriction = all methods allowed + } + return Arrays.stream(methods).anyMatch(m -> m.name().equalsIgnoreCase(method)); + } + } + + private static final List PUBLIC_ENDPOINTS = List.of( + // Auth endpoints - all methods + new PublicEndpoint("/api/user/auth/**"), + new PublicEndpoint("/api/user/test"), + new PublicEndpoint("/actuator/**"), + // Accommodation endpoints - GET only + new PublicEndpoint("/api/accommodation", HttpMethod.GET), + new PublicEndpoint("/api/accommodation/*", HttpMethod.GET), + new PublicEndpoint("/api/accommodation/host/*", HttpMethod.GET), + new PublicEndpoint("/api/accommodation/*/photos", HttpMethod.GET), + new PublicEndpoint("/api/accommodation/*/photos/*", HttpMethod.GET), + // Availability endpoints - GET only + new PublicEndpoint("/api/accommodation/*/availability", HttpMethod.GET), + new PublicEndpoint("/api/accommodation/*/availability/*", HttpMethod.GET), + // Rating endpoints - GET only + new PublicEndpoint("/api/rating", HttpMethod.GET), + new PublicEndpoint("/api/rating/target/*", HttpMethod.GET) + ); + + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + private final JwtService jwtService; + + public JwtAuthenticationFilter(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String path = request.getRequestURI(); + String method = request.getMethod(); + + if (isPublicEndpoint(path, method)) { + filterChain.doFilter(request, response); + return; + } + + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + sendError(response, "Missing or invalid Authorization header"); + return; + } + + try { + String token = authHeader.substring(7); + Claims claims = jwtService.parseToken(token); + + String userId = jwtService.getUserId(claims); + String role = jwtService.getRole(claims); + + if (userId == null || role == null) { + sendError(response, "Invalid token claims"); + return; + } + + HttpServletRequest wrappedRequest = new HeaderAddingRequestWrapper(request, Map.of( + "X-User-Id", userId, + "X-User-Role", role + )); + + filterChain.doFilter(wrappedRequest, response); + } catch (JwtException e) { + sendError(response, "Invalid or expired token"); + } + } + + private boolean isPublicEndpoint(String path, String method) { + return PUBLIC_ENDPOINTS.stream() + .anyMatch(endpoint -> + pathMatcher.match(endpoint.pattern(), path) && endpoint.matchesMethod(method)); + } + + private void sendError(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.setContentType("application/json"); + response.getWriter().write("{\"status\":401,\"detail\":\"%s\"}".formatted(message)); + } + + private static class HeaderAddingRequestWrapper extends HttpServletRequestWrapper { + private final Map extraHeaders; + + public HeaderAddingRequestWrapper(HttpServletRequest request, Map extraHeaders) { + super(request); + this.extraHeaders = extraHeaders; + } + + @Override + public String getHeader(String name) { + if (extraHeaders.containsKey(name)) { + return extraHeaders.get(name); + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaders(String name) { + if (extraHeaders.containsKey(name)) { + return Collections.enumeration(List.of(extraHeaders.get(name))); + } + return super.getHeaders(name); + } + + @Override + public Enumeration getHeaderNames() { + Set names = new LinkedHashSet<>(); + Enumeration original = super.getHeaderNames(); + while (original.hasMoreElements()) { + names.add(original.nextElement()); + } + names.addAll(extraHeaders.keySet()); + return Collections.enumeration(names); + } + } +} diff --git a/src/main/java/com/devoops/gateway/filter/TrafficMetricsFilter.java b/src/main/java/com/devoops/gateway/filter/TrafficMetricsFilter.java new file mode 100644 index 0000000..0ef36e9 --- /dev/null +++ b/src/main/java/com/devoops/gateway/filter/TrafficMetricsFilter.java @@ -0,0 +1,57 @@ +package com.devoops.gateway.filter; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class TrafficMetricsFilter extends OncePerRequestFilter { + + private final Counter requestBytesCounter; + private final Counter responseBytesCounter; + + public TrafficMetricsFilter(MeterRegistry registry) { + this.requestBytesCounter = Counter.builder("http_traffic_bytes_request_total") + .description("Total bytes received in HTTP requests") + .register(registry); + this.responseBytesCounter = Counter.builder("http_traffic_bytes_response_total") + .description("Total bytes sent in HTTP responses") + .register(registry); + } + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request, 10 * 1024 * 1024); + ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); + + try { + filterChain.doFilter(wrappedRequest, wrappedResponse); + } finally { + long requestBytes = wrappedRequest.getContentLength(); + if (requestBytes < 0) { + requestBytes = wrappedRequest.getContentAsByteArray().length; + } + requestBytesCounter.increment(requestBytes); + + long responseBytes = wrappedResponse.getContentSize(); + responseBytesCounter.increment(responseBytes); + + wrappedResponse.copyBodyToResponse(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/devoops/gateway/filter/UniqueVisitorFilter.java b/src/main/java/com/devoops/gateway/filter/UniqueVisitorFilter.java new file mode 100644 index 0000000..3b8c0cb --- /dev/null +++ b/src/main/java/com/devoops/gateway/filter/UniqueVisitorFilter.java @@ -0,0 +1,53 @@ +package com.devoops.gateway.filter; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class UniqueVisitorFilter extends OncePerRequestFilter { + + private final MeterRegistry registry; + + public UniqueVisitorFilter(MeterRegistry registry) { + this.registry = registry; + } + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String ip = extractClientIp(request); + String userAgent = request.getHeader("User-Agent"); + String visitorHash = computeHash(ip, userAgent); + + Counter.builder("http_visitor_request_total") + .description("HTTP requests with visitor fingerprint") + .tag("visitor_hash", visitorHash) + .register(registry) + .increment(); + + filterChain.doFilter(request, response); + } + + private String extractClientIp(HttpServletRequest request) { + String xff = request.getHeader("X-Forwarded-For"); + if (xff != null && !xff.isEmpty()) { + return xff.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } + + private String computeHash(String ip, String userAgent) { + String input = ip + "|" + (userAgent != null ? userAgent : "unknown"); + return Integer.toHexString(input.hashCode()); + } +} \ No newline at end of file diff --git a/src/main/java/com/devoops/gateway/service/JwtService.java b/src/main/java/com/devoops/gateway/service/JwtService.java new file mode 100644 index 0000000..8ee4f3a --- /dev/null +++ b/src/main/java/com/devoops/gateway/service/JwtService.java @@ -0,0 +1,36 @@ +package com.devoops.gateway.service; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; + +@Service +public class JwtService { + + private final SecretKey signingKey; + + public JwtService(@Value("${jwt.secret}") String secret) { + this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret)); + } + + public Claims parseToken(String token) { + return Jwts.parser() + .verifyWith(signingKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public String getUserId(Claims claims) { + return claims.get("userId", String.class); + } + + public String getRole(Claims claims) { + return claims.get("role", String.class); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6365994..4254648 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,46 @@ spring.application.name=gateway +server.port=${SERVER_PORT:8080} + +#spring.main.web-application-type=reactive +spring.cloud.gateway.server.webmvc.routes[0].id=user-service +spring.cloud.gateway.server.webmvc.routes[0].uri=${USER_SERVICE_URL:http://localhost:8081} +spring.cloud.gateway.server.webmvc.routes[0].predicates[0]=Path=/api/user/** + +spring.cloud.gateway.server.webmvc.routes[1].id=accommodation-service +spring.cloud.gateway.server.webmvc.routes[1].uri=${ACCOMMODATION_SERVICE_URL:http://localhost:8082} +spring.cloud.gateway.server.webmvc.routes[1].predicates[0]=Path=/api/accommodation/** + +spring.cloud.gateway.server.webmvc.routes[2].id=notification-service +spring.cloud.gateway.server.webmvc.routes[2].uri=${NOTIFICATION_SERVICE_URL:http://localhost:8083} +spring.cloud.gateway.server.webmvc.routes[2].predicates[0]=Path=/api/notification/** + +spring.cloud.gateway.server.webmvc.routes[3].id=rating-service +spring.cloud.gateway.server.webmvc.routes[3].uri=${RATING_SERVICE_URL:http://localhost:8084} +spring.cloud.gateway.server.webmvc.routes[3].predicates[0]=Path=/api/rating/** + +spring.cloud.gateway.server.webmvc.routes[4].id=reservation-service +spring.cloud.gateway.server.webmvc.routes[4].uri=${RESERVATION_SERVICE_URL:http://localhost:8085} +spring.cloud.gateway.server.webmvc.routes[4].predicates[0]=Path=/api/reservation/** + +spring.cloud.gateway.server.webmvc.routes[5].id=search-service +spring.cloud.gateway.server.webmvc.routes[5].uri=${SEARCH_SERVICE_URL:http://localhost:8086} +spring.cloud.gateway.server.webmvc.routes[5].predicates[0]=Path=/api/search/** + +# Logging configuration +logging.logstash.host=${LOGSTASH_HOST:localhost:5000} +logging.level.root=INFO +logging.level.com.devoops=DEBUG +logging.level.org.springframework.web=INFO +logging.level.org.springframework.cloud.gateway=INFO + +# JWT +jwt.secret=${JWT_SECRET:dGhpcyBpcyBhIHZlcnkgc2VjdXJlIGtleSBmb3IgSFMyNTYgdGhhdCBpcyBhdCBsZWFzdCAyNTYgYml0cyBsb25n} + +#Zipkin tracing configuration +management.tracing.sampling.probability=1.0 +management.tracing.export.zipkin.endpoint=http://${ZIPKIN_HOST:zipkin}:${ZIPKIN_PORT:9411}/api/v2/spans + +# Actuator endpoints for Prometheus metrics +management.endpoints.web.exposure.include=health,prometheus,metrics +management.endpoint.health.show-details=always +management.prometheus.metrics.export.enabled=true diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..eaa5def --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,76 @@ + + + + + + + + + {"service.name":"${applicationName}","host.name":"${HOSTNAME:-unknown}"} + + @timestamp + message + logger + thread + level + + + + + + + ${logstashHost} + + {"service.name":"${applicationName}","host.name":"${HOSTNAME:-unknown}"} + + @timestamp + message + logger + thread + level + [ignore] + + requestId + userId + + + + + + + 512 + 0 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml new file mode 100644 index 0000000..8c647eb --- /dev/null +++ b/src/test/resources/logback-test.xml @@ -0,0 +1,18 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + +