Przejdź do sekcji TLDR celem uzyskania szybkiej odpowiedzi na pytanie w tytule artykułu.
Zapisz się na newsletter, jeżeli nie chcesz przegapić kolejnych publikacji.
- Studium przypadku
- Rozwiązanie z użyciem Django middleware
- PoC
- TLDR
- Przydatne materiały
Studium przypadku
W środowisku rozproszonym, opartym o mikro-serwisy, istotną rolę odgrywa instrumentacja (ang. instrumentation), czyli dodanie fragmentów kodu, które umożliwiają dokładne monitorowanie działania aplikacji oraz mierzenie jej wydajności. Dzięki temu jesteśmy w stanie skutecznie zidentyfikować usługę, która może stanowić wąskie gardło całego systemu.
W jednym z projektów Django, nad którym swego czasu pracowałem, zaimplementowano instrumentację z wykorzystaniem narzędzi OpenTelemetry. Do propagacji kontekstu użyto propagatora B3, zgodnego ze standardem OpenTelemetry, który informacje o kontekście przekazuje poprzez nagłówki HTTP, o nazwie rozpoczynającej się prefiksem X-B3-.
W pewnym momencie zaszła potrzeba uruchomienia projektu na chmurze Google (Google Cloud Platform) i okazało się, iż nasza usługa chmurowa Google używa trochę innego nagłówka do przekazywania informacji o kontekście, a mianowicie: X-Cloud-Trace-Context.
Bez wdawania się w szczegóły decyzyjne: nie chcieliśmy na tym etapie zmieniać sposobu propagacji w usłudze, więc musieliśmy odczytać wartość trace_id z nagłówka X-Cloud-Trace-Context i umieścić ją w innym nagłówku X-B3-TraceId na poziomie żądania (HTTP request), zanim jeszcze zostanie obsłużone przez kod widoków.
Rozwiązanie z użyciem Django middleware
Żeby zmodyfikować samo żądanie HTTP, modyfikując nagłówki, najlepiej posłużyć się mechanizmem middleware. W dosłownym tłumaczeniu jest to oprogramowanie pośredniczące i możemy je sobie wyobrazić jako system wtyczek, przez które przechodzą żądania (requests) oraz odpowiedzi (responses) HTTP podczas standardowej komunikacji:
W uproszczeniu: kod middleware ma dostęp do danych żądania (HttpRequest), zanim zostanie ono obsłużone przez metodę widoku, a także do danych odpowiedzi (HttpResponse) zwróconej przez widok, zanim trafi ona do użytkownika.
Django pozwala na definiowanie własnych klas/funkcji middleware, które później należy zarejestrować w ustawieniu settings.MIDDLEWARE.
Warto zwrócić uwagę, iż kolejność rejestracji komponentów middleware ma znaczenie. Na powyższym schemacie, żądanie zostanie obsłużone w następującej kolejności: Middleware 1 -> Middleware 2 -> Middleware 3. Odpowiedź natomiast przejdzie przez warstwę pośredniczącą w odwrotnej kolejności: Middleware 3 -> Middleware 2 -> Middleware 1.
Podsumowując, komponent middleware jest odpowiednim miejscem na realizację naszego założenia, czyli przepisania wartości odczytanej z nagłówka X-Cloud-Trace-Context i umieszczeniu jej w innym nagłówku żądania HTTP.
PoC
- Stwórzmy możliwie najprostszy projekt Django i dodajmy do niego aplikację o nazwie telemetry. jeżeli nigdy wcześniej nie pracowałeś(-aś) z Django, polecam zapoznać się z oficjalnym wprowadzeniem.
- Zdefiniujmy prostą metodę widoku (dostępną pod ścieżką /telemetry), której zadaniem jest zwrócenie nagłówków żądania HTTP w formacie JSON:
- Po uruchomieniu aplikacji z użyciem lokalnego serwera deweloperskiego (python manage.py runserver), możemy wysłać żądanie z ustawionym nagłówkiem X-B3-Traceid i ujrzeć przykładową odpowiedź (przejrzyście sformatowaną dzięki narzędziu jq):
- Zgodnie z podejściem TDD (Test-Driven Development), zacznijmy od napisania automatycznych testów funkcjonalnych. Rozważmy 2 proste scenariusze:
- Fragment wartości nagłówka X-Cloud-Trace-Context jest przeniesiony do X-B3-Traceid, jeżeli takowego nie ma w żądaniu.
- Jeśli nagłówek X-B3-Traceid jest już zdefiniowany w żądaniu, to zignoruj nagłówek X-Cloud-Trace-Context.
- Implementacja klasy TelemetryMiddleware, realizującej pożądaną przez nas funkcjonalność, wymaga prawidłowego sparsowania zawartości nagłówka X-Cloud-Trace-Context. Co prawda nie jest to trudne zadanie, ale zamiast wyważać otwarte drzwi, możemy podejrzeć, jak to zostało zrobione w udostępnionej przez Google bibliotece open source: opentelemetry-operations-python.
- Warto zwrócić uwagę na fragment kodu request.__dict__.pop("headers", None), który powoduje, iż wartość HttpRequest.headers (cached property) zostanie odświeżona przy kolejnej próbie odczytu. Jest to konieczne, żeby adekwatność zawierała wprowadzone przez nas zmiany do request.META.
- Ostatnim krokiem jest zarejestrowanie klasy middleware w ustawieniach:
- Po uruchomieniu testów (python manage.py test), wszystkie powinny zakończyć się sukcesem.
TLDR
Podsumowując, jeżeli chcemy zmodyfikować nagłówki żądania HTTP przed obsłużeniem przez kod widoków Django:
- Definiujemy klasę (lub metodę) middleware, w której modyfikujemy nagłówki żądania:
- Rejestrujemy komponent w ustawieniach settings.py:
Przydatne materiały
- Unit Testing Django Middleware – jak przetestować sam komponent middleware przy użyciu testów jednostkowych.
- StackOverflow: Django overwrite header value in request object – odrobinę szersze wyjaśnienie dotyczące odświeżenia adekwatności HttpRequest.headers.