diff --git a/.gitignore b/.gitignore index e003ee6..66c5b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -356,3 +356,11 @@ EventHubSamplingOverrides/output.txt EventHubSamplingOverrides/target/* target/ + + +application-local.yml +application-local.yaml +application-local.properties +/applicationinsights.log +it-test-local.properties +/docker/**/data/ diff --git a/insightCustomDimensions/Docker/applicationinsights-agent-3.7.4.jar b/insightCustomDimensions/Docker/applicationinsights-agent-3.7.4.jar new file mode 100644 index 0000000..51cfeb3 Binary files /dev/null and b/insightCustomDimensions/Docker/applicationinsights-agent-3.7.4.jar differ diff --git a/insightCustomDimensions/Docker/applicationinsights.json b/insightCustomDimensions/Docker/applicationinsights.json new file mode 100644 index 0000000..29f5de9 --- /dev/null +++ b/insightCustomDimensions/Docker/applicationinsights.json @@ -0,0 +1,32 @@ +{ + "selfDiagnostics": { + "destination": "console", + "level": "DEBUG" + }, + "instrumentation": { + "logging": { + "enabled": true, + "level": "DEBUG" + }, + "jdbc": { + "enabled": true, + "masking": { + "enabled": false + } + }, + "springScheduling": { + "enabled": true + } + }, + "customDimensions": { + }, + "sampling": { + "percentage": 100 + }, + "preview": { + "captureLogbackMarker": false, + "captureLogbackCodeAttributes": false, + "captureLoggingLevelAsCustomDimension": true + } + +} \ No newline at end of file diff --git a/insightCustomDimensions/Docker/applicationinsights_to_file.json b/insightCustomDimensions/Docker/applicationinsights_to_file.json new file mode 100644 index 0000000..45bdb33 --- /dev/null +++ b/insightCustomDimensions/Docker/applicationinsights_to_file.json @@ -0,0 +1,37 @@ +{ + "selfDiagnostics": { + "destination": "file+console", + "level": "DEBUG", + "file": { + "path": "/tmp/applicationinsights.log", + "maxSizeMb": 5, + "maxHistory": 1 + } + }, + "instrumentation": { + "logging": { + "enabled": true, + "level": "DEBUG" + }, + "jdbc": { + "enabled": true, + "masking": { + "enabled": false + } + }, + "springScheduling": { + "enabled": true + } + }, + "customDimensions": { + }, + "sampling": { + "percentage": 100 + }, + "preview": { + "captureLogbackMarker": false, + "captureLogbackCodeAttributes": false, + "captureLoggingLevelAsCustomDimension": true + } + +} \ No newline at end of file diff --git a/insightCustomDimensions/Dockerfile b/insightCustomDimensions/Dockerfile new file mode 100644 index 0000000..580e217 --- /dev/null +++ b/insightCustomDimensions/Dockerfile @@ -0,0 +1,41 @@ +# ---- Builder Stage ---- +FROM maven:3.9.6-eclipse-temurin-17 AS builder + +WORKDIR /app + +# Copy pom.xml and preload dependencies +COPY pom.xml . +RUN mvn dependency:go-offline -B + +# Copy sources and build +COPY src ./src +RUN mvn clean package -DskipTests + +# Extract layers +RUN java -Djarmode=layertools -jar /app/target/*.jar extract + + +# the second stage of our build will copy the extracted layers +FROM eclipse-temurin:17-jre-alpine + +WORKDIR /application + +COPY --from=builder /app/dependencies/ ./dependencies/ +COPY --from=builder /app/snapshot-dependencies/ ./snapshot-dependencies/ +COPY --from=builder /app/spring-boot-loader/ ./spring-boot-loader/ +COPY --from=builder /app/application/ ./application/ + +# Copy Application Insights agent + config +COPY ./Docker/applicationinsights-agent-3.7.4.jar /application/insight/ +COPY ./Docker/applicationinsights.json /application/insight/ + +# Set AI config +ENV APPLICATIONINSIGHTS_CONFIGURATION_FILE=/application/insight/applicationinsights.json +# Create a group and user +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +# Tell docker that all future commands should run as the appuser user +USER appuser + +EXPOSE 8080 +# Run Spring Boot via JarLauncher with AI agent +ENTRYPOINT ["java", "-javaagent:/application/insight/applicationinsights-agent-3.7.4.jar","org.springframework.boot.loader.launch.JarLauncher"] diff --git a/insightCustomDimensions/README.md b/insightCustomDimensions/README.md new file mode 100644 index 0000000..ab1f5a1 --- /dev/null +++ b/insightCustomDimensions/README.md @@ -0,0 +1,66 @@ +* A single controller that receives HTTP requests +* A request filter similar to the one you have in your code using the @Order tag. +* Inside the filter, create your custom span and apply the filter as you currently do in your code. +* Collect a dummy custom property that is expected to be added to your request. +* Enable App Insights Java Agent 3.7.2 +* Use the same applicationinsights.json file that you have on your current Application. +* Even if this dummy code does not reproduce the issue, it will help our engineering team to + understand this scenario. + +To Reproduce +Submitting a PR with an example reproducing the issue +in [this repository](https://github.com/microsoft/ApplicationInsights-Java-Repros) would make it +easier for the Application Insight maintainers to help you. Before doing this, you have to fork this +repository. + +## Sample Test App + +* 5 Filter added + * `SpanInitializationFilter`: init the span + * `OidLoggingFilter`: read the OID from JWT Baerer if exists + * `CountryLoggingFilter`: read the dbName from URL like /api/v1/{dbName}/company + * `HttpQueryLoggingFilter`: read the Query-Parameter + * `RawBodyLoggingFilter`: read the body of POST and PUT +* Endpoints: + * GET /api/v1/:dbName/company (i.e. :dbName=germany) + * Response: + ``` + { + "id": 1758202622283, + "name": "germany-Name", + "street": "germany-Street", + "postalCode": "germany-PostalCode", + "city": "germany-City" + }, + { + "id": 1758202622283, + "name": "germany-Name", + "street": "germany-Street", + "postalCode": "germany-PostalCode", + "city": "germany-City" + } + ] + ``` + * GET /api/v1/:dbName/company/:id (i.e :dbName=germany, id=123) + * Response: + ``` + { + "id": 111, + "name": "germany-Name", + "street": "germany-Street", + "postalCode": "germany-PostalCode", + "city": "germany-City" + } + ``` + + * POST/PUT /api/v1/:dbName/company (i.e :dbName=germany) + * Request/Response (sample) + ``` + { + "id": 111, + "name": "germany-Name", + "street": "germany-Street", + "postalCode": "germany-PostalCode", + "city": "germany-City" + } + ``` \ No newline at end of file diff --git a/insightCustomDimensions/pom.xml b/insightCustomDimensions/pom.xml new file mode 100644 index 0000000..4eca459 --- /dev/null +++ b/insightCustomDimensions/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + com.peri.psd + testapp + 0.0.1-SNAPSHOT + testapp + Test App + + + + + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + io.micrometer + micrometer-tracing-bridge-otel + + + + com.nimbusds + nimbus-jose-jwt + 10.4.2 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + + \ No newline at end of file diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/PsdTestApplication.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/PsdTestApplication.java new file mode 100644 index 0000000..2814f64 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/PsdTestApplication.java @@ -0,0 +1,14 @@ +package com.peri.psd.testapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@SpringBootApplication(scanBasePackages = {"com.peri.psd.testapp"}) +@EnableConfigurationProperties +public class PsdTestApplication { + public static void main(String[] args) { + SpringApplication.run(PsdTestApplication.class, args); + } + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/api/PsdTestController.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/api/PsdTestController.java new file mode 100644 index 0000000..b1aad4d --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/api/PsdTestController.java @@ -0,0 +1,73 @@ +package com.peri.psd.testapp.playground.api; + +import com.peri.psd.testapp.playground.dto.CompanyMasterDto; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("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/api/v1") +@Slf4j +@RequiredArgsConstructor +public class PsdTestController { + + + @GetMapping("/{dbName}/company/{id}") + public ResponseEntity getCompanyById( + @PathVariable String dbName, + @PathVariable Long id) { + + log.debug("GET: /{}/company/{}", dbName, id); + + return ResponseEntity.ok(CompanyMasterDto.builder() + .id(id) + .name(dbName + "-Name") + .city(dbName + "-City") + .street(dbName + "-Street") + .postalCode(dbName + "-PostalCode") + .build()); + } + + @GetMapping("/{dbName}/company") + public ResponseEntity> getAllCompanies(@PathVariable String dbName) { + + log.debug("GET: /{}/company", dbName); + return ResponseEntity.ok(List.of(CompanyMasterDto.builder() + .id(System.currentTimeMillis()) + .name(dbName + "-Name") + .city(dbName + "-City") + .street(dbName + "-Street") + .postalCode(dbName + "-PostalCode") + .build(), + CompanyMasterDto.builder() + .id(System.currentTimeMillis()) + .name(dbName + "-Name") + .city(dbName + "-City") + .street(dbName + "-Street") + .postalCode(dbName + "-PostalCode") + .build())); + } + + @PostMapping("/{dbName}/company") + public ResponseEntity createCompany(@PathVariable String dbName, + @RequestBody CompanyMasterDto companyMasterDto) { + + log.debug("POST: /{}/company - {}", dbName, companyMasterDto); + return ResponseEntity.status(HttpStatus.CREATED) + .body(companyMasterDto); + } + + @PutMapping("/{dbName}/company") + public ResponseEntity updateCompany(@PathVariable String dbName, + @RequestBody CompanyMasterDto companyMasterDto) { + + log.debug("PUT: /{}/company - {}", dbName, companyMasterDto); + return ResponseEntity.status(HttpStatus.ACCEPTED) + .body(companyMasterDto); + } + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/dto/CompanyMasterDto.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/dto/CompanyMasterDto.java new file mode 100644 index 0000000..78af265 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/dto/CompanyMasterDto.java @@ -0,0 +1,19 @@ +package com.peri.psd.testapp.playground.dto; + +import lombok.*; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class CompanyMasterDto { + + private Long id; + private String name; + private String street; + private String postalCode; + private String city; + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/AbstractLoggingFilter.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/AbstractLoggingFilter.java new file mode 100644 index 0000000..f95b357 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/AbstractLoggingFilter.java @@ -0,0 +1,54 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import io.opentelemetry.api.trace.Span; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.util.List; + +@Slf4j +public abstract class AbstractLoggingFilter extends OncePerRequestFilter { + + + final static String KEY_CALLER = "caller"; + final static String KEY_COUNTRY = "country"; + final static String KEY_REQUESTQUERY = "requestQuery"; + final static String KEY_REQUESTBODYRAW = "requestBodyRaw"; + + // List of URL patterns to be exclude + private static final List EXCLUDED_PATHS = List.of( + "/actuator" + ); + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String path = request.getRequestURI(); + + return "/".equals(path) || EXCLUDED_PATHS.stream() + .anyMatch(path::startsWith); + } + + public void sendToSpan(String key, String value) { + + Span currentSpan = Span.current(); + String spanKind = MDC.get("MDC.span.kind"); + + if (currentSpan != null && currentSpan.getSpanContext() + .isValid() && "SERVER".equals(spanKind)) { + + log.debug("Span: Kind={} - Attribute: {}={}", spanKind, key, value); + + MDC.put("MDC." + key, value); + + currentSpan.setAttribute(key, value); + } else { + log.debug("REQUEST Attribute: {} = {}", key, value); + } + + } + + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/CountryLoggingFilter.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/CountryLoggingFilter.java new file mode 100644 index 0000000..cd12e6b --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/CountryLoggingFilter.java @@ -0,0 +1,71 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +@ConditionalOnProperty(name = "psd.azure.insight.filter.country-filter", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +@Slf4j +@Order(SecurityProperties.DEFAULT_FILTER_ORDER + 120) +public class CountryLoggingFilter extends AbstractLoggingFilter { + + // https://petrepopescu.tech/2023/07/how-to-log-http-request-and-response-in-spring-boot/ + // https://www.linkedin.com/pulse/rest-api-monitoring-azure-application-insights-srinivas-anumala + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + RepeatableContentCachingRequestWrapper requestWrapper = new RepeatableContentCachingRequestWrapper( + request); + + logRequest(requestWrapper); + + filterChain.doFilter(request, response); + + } + + private void logRequest( + RepeatableContentCachingRequestWrapper requestWrapper) throws IOException { + extractCurrentDbFromUri(requestWrapper.getRequestURI()) + .ifPresent(currentDb -> + sendToSpan(KEY_COUNTRY, currentDb) + ); + } + + /** + * @param uri i.e /api/v3/germany/foo or /germany/api/v3/foo + * @return germany + */ + public static Optional extractCurrentDbFromUri(String uri) { + if (uri == null || uri.trim() + .isEmpty()) { + return Optional.empty(); + } + + Pattern pattern = Pattern.compile("^/([^/]+)/api/v\\d+/.+|^/api/v\\d+/([^/]+)/.+", + Pattern.CASE_INSENSITIVE); + + Matcher matcher = pattern.matcher(uri); + if (matcher.find()) { + // Group 1: db before /api/vXX, Group 2: db after /api/vXX + return Optional.ofNullable( + matcher.group(1) != null ? matcher.group(1) : matcher.group(2) + ); + } + return Optional.empty(); + } + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/HttpQueryLoggingFilter.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/HttpQueryLoggingFilter.java new file mode 100644 index 0000000..a0860bd --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/HttpQueryLoggingFilter.java @@ -0,0 +1,54 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.stream.Stream; + +@Component +@ConditionalOnProperty(name = "psd.azure.insight.filter.query-filter", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +@Slf4j +@Order(SecurityProperties.DEFAULT_FILTER_ORDER + 130) +public class HttpQueryLoggingFilter extends AbstractLoggingFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + RepeatableContentCachingRequestWrapper requestWrapper = new RepeatableContentCachingRequestWrapper( + request); + + logQueryParameter(requestWrapper); + + filterChain.doFilter(request, response); + + } + + private void logQueryParameter( + RepeatableContentCachingRequestWrapper requestWrapper) throws IOException { + + String queryAsString = requestWrapper.getQueryString(); + + if (shouldLogQuery(requestWrapper)) { + sendToSpan(KEY_REQUESTQUERY, queryAsString); + } + } + + private boolean shouldLogQuery(RepeatableContentCachingRequestWrapper requestWrapper) { + String method = requestWrapper.getMethod(); + boolean b = Stream.of("GET", "POST", "PUT", "PATCH", "DELETE") + .anyMatch(m -> m.equalsIgnoreCase(method)); + log.trace("SHOULD LOG = {} - {}:{}", b, method, requestWrapper.getServletPath()); + return b; + } + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/OidLoggingFilter.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/OidLoggingFilter.java new file mode 100644 index 0000000..9ca71fb --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/OidLoggingFilter.java @@ -0,0 +1,62 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import com.nimbusds.jwt.SignedJWT; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.text.ParseException; + +@Component +@ConditionalOnProperty(name = "psd.azure.insight.filter.oid-filter", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +@Slf4j +@Order(SecurityProperties.DEFAULT_FILTER_ORDER + 110) +public class OidLoggingFilter extends AbstractLoggingFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + RepeatableContentCachingRequestWrapper requestWrapper = new RepeatableContentCachingRequestWrapper( + request); + + logRequest(requestWrapper); + + filterChain.doFilter(request, response); + + } + + private void logRequest( + RepeatableContentCachingRequestWrapper requestWrapper) throws IOException { + + String authHeader = requestWrapper.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String jwt = authHeader.substring(7); // Token without "Bearer " + + try { + SignedJWT parsed = SignedJWT.parse(jwt); + String oid = parsed.getJWTClaimsSet() + .getClaim("oid") + .toString(); + sendToSpan(KEY_CALLER, oid); + + } catch (ParseException e) { + log.error("JWT parse error", e); + } + + } else { + log.warn("Request has no SecurityContext and no Bearer Token"); + } + + } + +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/RawBodyLoggingFilter.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/RawBodyLoggingFilter.java new file mode 100644 index 0000000..1ac9ea4 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/RawBodyLoggingFilter.java @@ -0,0 +1,77 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.util.ContentCachingResponseWrapper; + +import java.io.IOException; +import java.util.stream.Stream; + +@Component +@ConditionalOnProperty(name = "psd.azure.insight.filter.body-filter", havingValue = "true", matchIfMissing = true) +@RequiredArgsConstructor +@Slf4j +@Order(SecurityProperties.DEFAULT_FILTER_ORDER + 140) +public class RawBodyLoggingFilter extends AbstractLoggingFilter { + + private final int MAX_BODY_SIZE = 5000; + + // https://petrepopescu.tech/2023/07/how-to-log-http-request-and-response-in-spring-boot/ + // https://www.linkedin.com/pulse/rest-api-monitoring-azure-application-insights-srinivas-anumala + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + RepeatableContentCachingRequestWrapper requestWrapper = new RepeatableContentCachingRequestWrapper( + request); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + + logRequest(requestWrapper); + + filterChain.doFilter(requestWrapper, responseWrapper); + logResponse(responseWrapper); + + } + + private void logRequest( + RepeatableContentCachingRequestWrapper requestWrapper) throws IOException { + + // this must be read always + String body = requestWrapper.readInputAndDuplicate(); + if (body.length() > MAX_BODY_SIZE) { + log.trace("trim request from {} to {}", body.length(), + MAX_BODY_SIZE); + body = body.substring(0, MAX_BODY_SIZE); + } + + if (shouldLogRequest(requestWrapper)) { + sendToSpan(KEY_REQUESTBODYRAW, body); + } + } + + private void logResponse(ContentCachingResponseWrapper responseWrapper) throws IOException { + if (shouldLogResponsePayload(responseWrapper)) { + log.debug("Response {}", new String(responseWrapper.getContentAsByteArray())); + } + responseWrapper.copyBodyToResponse(); + } + + private boolean shouldLogRequest(RepeatableContentCachingRequestWrapper requestWrapper) { + String method = requestWrapper.getMethod(); + boolean b = Stream.of("POST", "PUT", "PATCH") + .anyMatch(m -> m.equalsIgnoreCase(method)); + log.trace("SHOULD LOG = {} - {}:{}", b, method, requestWrapper.getServletPath()); + return b; + } + + private boolean shouldLogResponsePayload(ContentCachingResponseWrapper responseWrapper) { + return false; + } +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/RepeatableContentCachingRequestWrapper.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/RepeatableContentCachingRequestWrapper.java new file mode 100644 index 0000000..4dc48c0 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/RepeatableContentCachingRequestWrapper.java @@ -0,0 +1,29 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.IOException; + +public class RepeatableContentCachingRequestWrapper extends ContentCachingRequestWrapper { + private SimpleServletInputStream inputStream; + + public RepeatableContentCachingRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public ServletInputStream getInputStream() { + return this.inputStream; + } + + public String readInputAndDuplicate() throws IOException { + if (inputStream == null) { + byte[] body = super.getInputStream() + .readAllBytes(); + this.inputStream = new SimpleServletInputStream(body); + } + return new String(super.getContentAsByteArray()); + } +} diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/SimpleServletInputStream.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/SimpleServletInputStream.java new file mode 100644 index 0000000..6d84862 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/SimpleServletInputStream.java @@ -0,0 +1,36 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class SimpleServletInputStream extends ServletInputStream { + private InputStream delegate; + + public SimpleServletInputStream(byte[] data) { + this.delegate = new ByteArrayInputStream(data); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public int read() throws IOException { + return this.delegate.read(); + } +} \ No newline at end of file diff --git a/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/SpanInitializationFilter.java b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/SpanInitializationFilter.java new file mode 100644 index 0000000..f09e097 --- /dev/null +++ b/insightCustomDimensions/src/main/java/com/peri/psd/testapp/playground/telemetry/filter/SpanInitializationFilter.java @@ -0,0 +1,54 @@ +package com.peri.psd.testapp.playground.telemetry.filter; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@Order(SecurityProperties.DEFAULT_FILTER_ORDER + 100)// Ensure it runs after the Security filters +@RequiredArgsConstructor +@Slf4j +public class SpanInitializationFilter extends AbstractLoggingFilter { + + private final Tracer tracer; + + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + // Create and activate a span for this request + Span span = tracer.spanBuilder(request.getMethod() + " " + request.getRequestURI()) + .setSpanKind(SpanKind.SERVER) + .startSpan(); + try (Scope scope = span.makeCurrent()) { + SpanContext spanContext = span.getSpanContext(); + MDC.put("MDC.span.kind", SpanKind.SERVER.name()); + span.setAttribute("span.kind", SpanKind.SERVER.name()); + log.debug("current SPAN {}", spanContext); + + filterChain.doFilter(request, response); + } finally { + log.debug("END current SPAN: {}", span.getSpanContext()); + MDC.clear(); + span.end(); + } + } + +} diff --git a/insightCustomDimensions/src/main/resources/application.yaml b/insightCustomDimensions/src/main/resources/application.yaml new file mode 100644 index 0000000..ed73296 --- /dev/null +++ b/insightCustomDimensions/src/main/resources/application.yaml @@ -0,0 +1,7 @@ +spring: + application: + name: psd-testapp + +logging: + level: + com.peri: DEBUG \ No newline at end of file