
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:
- Extract tracing context từ headers:
- Dùng OpenTelemetry
TextMapPropagatorđọctraceparent,baggage. - Nếu không có → tạo new root span (traceId mới).
- Dùng OpenTelemetry
- Sinh hoặc lấy
requestId:- Lấy từ header
X-Request-Idnế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ề).
- Lấy từ header
- 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].
- Gọi tiếp xuống chain:
chain.doFilter(request, response);- Lúc này request bắt đầu vào DispatcherServlet + Spring MVC.
- 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.
- Kết thúc span nếu span được start trong Filter:
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:
- Dùng HandlerMapping để tìm ra controller method tương ứng với URL + HTTP method.
- Dùng HandlerAdapter để gọi method đó.
- 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
requestIdtừ 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")
- Gắn business attributes:
- 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:
- 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 setSentry.setUser(...)). - Ghi stacktrace, breadcrumbs (các log trước đó).
- Nếu bạn đã tích hợp Sentry SDK với Spring:
- Trả error response về client:
HTTP 400/500vớ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.
- Log kết quả:
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:

Be First to Comment