Skip to content

Thiết lập OpenTelemetry và Sentry trong Spring Boot: Theo dấu từng request từ đầu đến cuối

Luồng HTTP request trong Spring Boot tích hợp OpenTelemetry + Sentry, và vị trí bơm/clear MDC (Mapped Diagnostic Context)

0. Client gửi HTTP request

Client (Browser/App/Postman) gửi request:

  • URL: GET /api/cards/123
  • Headers có thể gồm:
    • traceparent, baggage (nếu từ một hệ thống đã bật OpenTelemetry/Tracing).
    • X-Request-Id (nếu phía upstream đã sinh).
    • Auth header (Authorization: Bearer ...), user-agent, v.v.

👉 Từ đây trở đi, traceId (nếu có) sẽ được “chuyền tay” qua các layer.


1. Servlet Container & Filter (ObservabilityFilter + MDC + OpenTelemetry)

1.1. Servlet Container nhận request

Embedded server (Tomcat/Jetty) nhận TCP connection, parse HTTP → tạo HttpServletRequest, HttpServletResponse → đẩy qua Filter chain.

1.2. ObservabilityFilter (doFilter)

Ở đây ta có 1 filter kiểu:

public class ObservabilityFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // 1. Extract context
        // 2. Start span nếu cần
        // 3. Put MDC
        // 4. chain.doFilter(...)
        // 5. End span + MDC.clear() (finally)
    }
}

Bên trong nó thường làm:

  1. Extract tracing context từ headers:
    • Dùng OpenTelemetry TextMapPropagator đọc traceparent, baggage.
    • Nếu không có → tạo new root span (traceId mới).
  2. Sinh hoặc lấy requestId:
    • Lấy từ header X-Request-Id nếu có.
    • Nếu không → UUID.randomUUID() rồi set vào:
      • request.setAttribute("requestId", ...)
      • response.setHeader("X-Request-Id", ...) (thường set lúc trả về).
  3. Bơm vào MDC:
    • MDC.put("requestId", requestId);
    • MDC.put("traceId", currentSpan.getSpanContext().getTraceId());
    • MDC.put("spanId", currentSpan.getSpanContext().getSpanId());
      → Từ đây về sau, mọi log trong request này (ở thread hiện tại) đều tự có [requestId][traceId][spanId].
  4. Gọi tiếp xuống chain:
    • chain.doFilter(request, response);
    • Lúc này request bắt đầu vào DispatcherServlet + Spring MVC.
  5. Finally: cleanup
    • Kết thúc span nếu span được start trong Filter:
      • span.end();
    • Clear MDC:
      • MDC.clear();
        → Tránh rò rỉ context sang request khác.

2. DispatcherServlet & Interceptor (Spring MVC layer)

2.1. DispatcherServlet

Spring Boot đăng ký DispatcherServlet làm “front controller”.

Nó làm các việc:

  1. Dùng HandlerMapping để tìm ra controller method tương ứng với URL + HTTP method.
  2. Dùng HandlerAdapter để gọi method đó.
  3. Quản lý HandlerInterceptor trước và sau khi gọi controller.

2.2. Interceptor – preHandle()

Trước khi gọi controller, Spring chạy tất cả Interceptor đã đăng ký:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    // Lấy requestId, traceId, userId,...
    // Thêm business tag vào span
    // Log "request start"
    return true;
}

Ở đây thường làm:

  • Lấy lại requestId từ attribute/ header (do Filter set).
  • Dùng Span.current() (OpenTelemetry) để:
    • Gắn business attributes:
      • span.setAttribute("http.route", "/api/cards/{id}")
      • span.setAttribute("user.id", userId)
      • span.setAttribute("feature", "card-blocking")
  • Ghi thêm vào MDC:
    • MDC.put("userId", userId);
    • MDC.put("endpoint", "/api/cards/123");
  • Log: [requestId][traceId][spanId][userId] Incoming request GET /api/cards/123

2.3. Controller

Controller chính là chỗ xử lý nghiệp vụ HTTP:

@GetMapping("/api/cards/{id}")
public CardDto getCard(@PathVariable String id) {
    return cardService.getCardDetail(id);
}
  • Ở đây có thể throw exception (business error, validation error, etc.).
  • Mọi log trong controller vẫn “ăn” MDC: [requestId][traceId][spanId].

3. Service, Repository, Database & OpenTelemetry auto-instrumentation

3.1. Service layer

@Service chứa business logic:

public CardDto getCardDetail(String id) {
    // log info, debug
    // gọi repository, gọi external service,...
}

Nếu bạn bật OpenTelemetry auto-instrumentation:

  • Mỗi call tới HTTP client (WebClient, RestTemplate, OkHttp,…) sẽ được tạo child span tự động.
  • Mỗi call tới JDBC/JPA (Hikari + Hibernate) cũng có span đại diện cho query.

3.2. Repository layer

@Repository gọi DB, ví dụ JPA:

public Optional<CardEntity> findById(String id) {
    return cardRepository.findById(id);
}

OpenTelemetry:

  • Tạo span kiểu db.query:
    • db.system = "postgresql"
    • db.statement = "select ... from card where id = ?"
    • db.sql.table = "card"

Mọi span này đều dùng chung traceId, nên trên dashboard (Tempo/Jaeger/Zipkin, hoặc Sentry Performance) bạn sẽ thấy:

  • 1 trace gồm nhiều span:
    • HTTP server span (request vào).
    • HTTP client span (nếu call service khác).
    • DB spans.

4. Error path – @ControllerAdvice + Sentry

Giả sử ở Service/Controller có lỗi:

if (card.isBlocked()) {
    throw new BusinessException("CARD_BLOCKED");
}

Hoặc NPE, runtime exception.

4.1. @ControllerAdvice bắt lỗi

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handle(Exception ex, HttpServletRequest request) {
        // Đẩy sang Sentry
        // Trả JSON error về client
    }
}

Tại đây:

  1. Gửi lỗi lên Sentry:
    • Nếu bạn đã tích hợp Sentry SDK với Spring: Sentry.captureException(ex);
    • Sentry sẽ tự:
      • Gắn traceId/spanId từ context (nếu có OpenTelemetry integration).
      • Gắn requestId, headers, user info (nếu bạn set Sentry.setUser(...)).
      • Ghi stacktrace, breadcrumbs (các log trước đó).
  2. Trả error response về client:
    • HTTP 400/500 với body kiểu: { "requestId": "...", "errorCode": "CARD_BLOCKED", "message": "Card is blocked" }

Trên Sentry UI, bạn sẽ thấy:

  • Event error có:
    • tags.traceId = ...
    • tags.requestId = ...
  • Có thể click cross-link sang hệ thống trace (nếu cấu hình).

5. Interceptor – postHandle() / afterCompletion()

Sau khi controller chạy xong (dù thành công hay thất bại):

  • postHandle() (chỉ khi không exception) có thể:
    • Gắn thêm info vào model/view.
    • Đo processing time.
  • afterCompletion() luôn chạy:
    • Log kết quả: [requestId][traceId][spanId] Completed /api/cards/123 in 120ms status=200
    • Dùng được MDC nên log vẫn bám requestId/traceId.

6. Quay lại Filter → Clear MDC → Response về Client

Khi Spring trả về DispatcherServlet xong, call stack quay về Filter:

try {
    chain.doFilter(request, response);
} finally {
    span.end();
    MDC.clear();
}
  • span.end() kết thúc HTTP server span/ root span nếu bạn start tại Filter.
  • MDC.clear() xóa hết key (requestId, traceId, userId,…) tránh “dính” qua request khác.

Servlet Container gửi HTTP response lại về Client:

  • Status: 200, 400, 500,…
  • Headers:
    • X-Request-Id: ...
    • traceparent: ... (nếu bạn propagate lại).
  • Body: JSON/HTML tùy API.

7. Tóm tắt ngắn gọn

  • Filter:
    • Nơi extract / start trace, sinh requestId, bơm MDC, bắt đầu & kết thúc span “toàn request”.
  • Interceptor:
    • Nơi gắn business info (userId, feature, endpoint) vào span + MDC.
    • Log thời gian, status, error code.
  • Controller/Service/Repo:
    • Chạy logic; auto-instrumentation tạo child span cho HTTP client, DB, etc.
  • @ControllerAdvice + Sentry:
    • Bắt exception, gửi error lên Sentry kèm traceId/requestId → dễ điều tra.

Bổ sung

Luồng HTTP request theo trục thời gian:

Published inAll

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *