AlgoCoach AI
Java Lab/Common Interview Coding Patterns/Spring Boot: Exception Handling + Dynatrace Alerting
Common Interview Coding Patterns/Lesson 8
Not started

Spring Boot: Exception Handling + Dynatrace Alerting

advancedspring-bootexception-handlingControllerAdvice

Exception Handling + Dynatrace Alerting in Financial APIs

Why This Matters

Your team has @ControllerAdvice for logging but is missing the alert layer. In a financial domain (remittance, AML, fund pull), missing an alert means your team discovers outages from angry customers—not from Dynatrace. This lesson covers the full exception handling chain from request to alert.


The Architecture

HTTP Request
    ↓
Controller
    ↓ throws
Service / Repository
    ↓
@ControllerAdvice ←— catch, classify, mask PII, log
    ↓
Dynatrace Custom Event / Metric ←— THIS IS THE MISSING PIECE
    ↓
HTTP Response (standardized error body)

1. @ControllerAdvice With Masking and Log Levels

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(RemittanceBusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(RemittanceBusinessException ex,
                                                        HttpServletRequest request) {
        // Milestone log — INFO is enough, expected business rule violation
        log.info("[MILESTONE] Business exception on {}: code={}, msg={}",
                 request.getRequestURI(), ex.getErrorCode(), ex.getMessage());

        dynatraceAlert(ex, AlertSeverity.INFO, request);
        return ResponseEntity.unprocessableEntity()
                .body(ErrorResponse.of(ex.getErrorCode(), ex.getMessage()));
    }

    @ExceptionHandler(FundPullException.class)
    public ResponseEntity<ErrorResponse> handleFundPull(FundPullException ex,
                                                        HttpServletRequest request) {
        // Fund pull failure is a CRITICAL milestone — always WARN+
        String maskedAccount = mask(ex.getAccountNumber()); // ****1234
        log.warn("[MILESTONE][FUND_PULL] Failed for account={} txnId={} reason={}",
                 maskedAccount, ex.getTxnId(), ex.getReason());

        dynatraceAlert(ex, AlertSeverity.CRITICAL, request);
        return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
                .body(ErrorResponse.of("FUND_PULL_FAILED", "Unable to pull funds. Please retry."));
    }

    @ExceptionHandler(ThirdPartyApiException.class)
    public ResponseEntity<ErrorResponse> handleThirdParty(ThirdPartyApiException ex,
                                                          HttpServletRequest request) {
        log.error("[MILESTONE][THIRD_PARTY] provider={} status={} correlationId={}",
                  ex.getProviderName(), ex.getHttpStatus(), ex.getCorrelationId());

        dynatraceAlert(ex, AlertSeverity.HIGH, request);
        return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
                .body(ErrorResponse.of("PROVIDER_ERROR", "Third-party service unavailable."));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex,
                                                          HttpServletRequest request) {
        // Never log the full stack in INFO — only ERROR for unexpected
        log.error("[MILESTONE][UNEXPECTED] uri={} type={}",
                  request.getRequestURI(), ex.getClass().getSimpleName(), ex);

        dynatraceAlert(ex, AlertSeverity.CRITICAL, request);
        return ResponseEntity.internalServerError()
                .body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred."));
    }

    // PII masking utility
    private String mask(String value) {
        if (value == null || value.length() < 4) return "****";
        return "*".repeat(value.length() - 4) + value.substring(value.length() - 4);
    }
}

2. Dynatrace Integration — Three Options

Option A: Dynatrace OneAgent SDK (Java)

Add to pom.xml:

<dependency>
    <groupId>com.dynatrace.oneagent.sdk.java</groupId>
    <artifactId>oneagent-sdk</artifactId>
    <version>1.9.0</version>
</dependency>
@Component
public class DynatraceAlerter {

    private final OneAgentSDK sdk = OneAgentSDKFactory.createInstance();

    public void sendCustomEvent(String title, Map<String, String> props) {
        CustomRequestAttribute attr = sdk.addCustomRequestAttribute(title, "custom-event");
        // The SDK auto-attaches to the current OneAgent trace context
        // Dynatrace picks this up as a custom attribute on the active request
    }

    // For metric ingest (counts, gauges):
    public void incrementErrorMetric(String errorType) {
        // Use OneAgent metric API to push: remittance.errors[type=errorType] += 1
        sdk.createIntegerCounterMetric("remittance.errors", MetricUnit.NONE)
           .value(1, Collections.singletonMap("type", errorType));
    }
}

Option B: Dynatrace HTTP Events API (No SDK dependency)

Better for services where you can't use the OneAgent SDK.

@Component
@Slf4j
public class DynatraceEventPusher {

    @Value("${dynatrace.url}")        // https://<env>.live.dynatrace.com
    private String dynatraceUrl;

    @Value("${dynatrace.api-token}")  // scope: events.ingest, metrics.ingest
    private String apiToken;

    private final RestTemplate rest = new RestTemplate();

    public void pushEvent(String title, String description, AlertSeverity severity) {
        String url = dynatraceUrl + "/api/v2/events/ingest";
        Map<String, Object> body = Map.of(
            "eventType", toEventType(severity),
            "title", title,
            "properties", Map.of("description", description, "source", "remittance-service")
        );
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(apiToken);
        headers.setContentType(MediaType.APPLICATION_JSON);
        try {
            rest.postForEntity(url, new HttpEntity<>(body, headers), String.class);
        } catch (Exception e) {
            // Never let alerting break the main response path
            log.warn("Failed to push Dynatrace event: {}", e.getMessage());
        }
    }

    private String toEventType(AlertSeverity severity) {
        return switch (severity) {
            case CRITICAL -> "ERROR_EVENT";
            case HIGH     -> "PERFORMANCE_EVENT";
            default       -> "CUSTOM_INFO";
        };
    }
}

Option C: Micrometer + Dynatrace Metrics Exporter (Recommended for Spring Boot)

# application.yml
management:
  dynatrace:
    metrics:
      export:
        uri: ${DYNATRACE_URL}/api/v2/metrics/ingest
        api-token: ${DYNATRACE_API_TOKEN}
        enabled: true
@Component
public class RemittanceMetrics {

    private final Counter fundPullErrors;
    private final Counter amlRejections;
    private final Counter thirdPartyErrors;

    public RemittanceMetrics(MeterRegistry registry) {
        this.fundPullErrors  = Counter.builder("remittance.fund_pull.errors").register(registry);
        this.amlRejections   = Counter.builder("remittance.aml.rejections").register(registry);
        this.thirdPartyErrors= Counter.builder("remittance.third_party.errors").register(registry);
    }

    public void recordFundPullError()   { fundPullErrors.increment(); }
    public void recordAmlRejection()    { amlRejections.increment(); }
    public void recordThirdPartyError() { thirdPartyErrors.increment(); }
}

Then in Dynatrace UI: create Custom Chart from remittance.fund_pull.errors and set a Davis Anomaly Detector or a Fixed Threshold Alert (> 5 in 5 min → alert).


3. Log Level Strategy for Milestones

MilestoneConditionLog Level
Request receivedAlwaysDEBUG (trace id only)
AML scan startedAlwaysINFO
AML scan resultApprovedINFO, Rejected → WARN
Fund pull attemptAlwaysINFO
Fund pull success—INFO
Fund pull failure—ERROR + Dynatrace
Third-party callAlwaysINFO with provider name
Third-party timeout—WARN + retry count
Third-party failure (no retry left)—ERROR + Dynatrace
Saga rollback triggered—ERROR + Dynatrace
Callback receivedAlwaysINFO
Transaction complete—INFO

4. Standard Error Response

public record ErrorResponse(
    String code,
    String message,
    String traceId,    // from MDC — lets ops team correlate with Dynatrace
    Instant timestamp
) {
    public static ErrorResponse of(String code, String message) {
        return new ErrorResponse(code, message,
            MDC.get("traceId"), Instant.now());
    }
}

Interview Talking Points

  • "We have @ControllerAdvice as a global handler. Each exception type maps to a log level—INFO for expected business rules, WARN for retryable externals, ERROR for unexpected or financial-critical failures."
  • "We mask PII (account numbers, phone) before logging—only last 4 digits are visible."
  • "We're integrating Dynatrace via Micrometer metrics export. Each critical exception increments a counter, and we set anomaly alerts in Dynatrace UI."
  • "We always include traceId in the response body so the ops team can find the Dynatrace trace in one click."
JavaLabRunner.java
Loading...
All classes must be named JavaLabRunner
Output

Click Run to execute the code.