Flow PHP - Telemetry
Obserwowalność - czyli sposób na monitorowanie zachowania na podstawie emitowanych przez system sygnałów.
Od kilku lat na rynku dominuje jeden standard, który zyskuje coraz szerszą adaptację w usługach typu APM, jest nim Open Telemetry.
OpenTelemetry dostarcza nie tylko specyfikację, protokóły czy konwencje nazewnictwa. To także zbiór bibliotek i SDK pozwalający na "łatwą" adaptację w niemalże każdym systemie.
Dlaczego więc postanowiłem napisać własną bibliotekę, zamiast skorzystać z gotowych rozwiązań?
Wszystko zaczęło się od tego, że chciałem zintegrować DataFrame z Flow PHP (oraz kilka bibliotek jak np postgresql czy filesystem) z OpenTelemetry.
Pierwszym problemem, na który trafiłem to zależności, SDK OpenTelemetry
php: ^8.1ext-json: *nyholm/psr7-server: ^1.1open-telemetry/api: ^1.7open-telemetry/context: ^1.4open-telemetry/sem-conv: ^1.0php-http/discovery: ^1.14psr/http-client: ^1.0psr/http-client-implementation: ^1.0psr/http-factory-implementation: ^1.0psr/http-message: ^1.0.1|^2.0psr/log: ^1.1|^2.0|^3.0ramsey/uuid: ^3.0 || ^4.0symfony/polyfill-mbstring: ^1.23symfony/polyfill-php82: ^1.26tbachert/spi: ^1.0.5
Biorąc pod uwagę, że obecnie Flow PHP (w bazowej wersji) wymaga tylko:
php: ~8.3.0 || ~8.4.0 || ~8.5.0composer-runtime-api: ^2.0ext-json: *brick/math: ^0.14.2flow-php/array-dot: 0.32.0flow-php/filesystem: 0.32.0flow-php/types: 0.32.0psr/clock: ^1.0psr/simple-cache: ^1.0 || ^2.0 || ^3.0symfony/string: ^6.4 || ^7.3 || ^8.0webmozart/glob: ^3.0 || ^4.0
Ilość zależności praktycznie by się podwoiła, co w przypadku frameworka, który użytkownicy muszą dodawać do listy zależności swoich systemów staje się odrobinę problematyczne ze względu na potencjalne konflikty wersji.
Teoretycznie mógłbym uzależnić Flow jedynie od open-telemetry/api a sdk dorzucić jako zależność
opcjonalną. Jednak kiedy zacząłem głębiej przyglądać się jak zbudowane jest API stwierdziłem, że nie jest
ono kompatybilne z architekturą, którą przyjąłem w Flow.
Głównym problemem były dla mnie wszelkiego rodzaju globalne stany, singletony i inne mechanizmy mające w teorii ułatwić auto instrumentacje. Przykład OpenTelemetry Globals.
Mógłbym pewnie próbować obejść Globals, jednak patrząc na implementacje instrumentacji oparte o OpenTelemetry bardzo szybko trafiłbym na problemy związane z tym, że SDK jest bardzo ściśle powiązane z API, a API jest bardzo ściśle powiązane z globalnym stanem.
Tutaj musiałem stanąć przed wyborem. Separować kod Flow za pomocą dodatkowej abstrakcji, lub zbudować własne API będące jednocześnie abstrakcją dla telemetrii.
Biorąc pod uwagę, że samo API nie jest aż tak rozbudowane, podjąłem decyzję o zbudowaniu niezależnego API oraz
warstwy
transportu, które są zgodne z protokołem OTLP. Tak oto powstała malutka biblioteczka o nazwie
flow-php/telemetry
Samo API flow-php/telemetry jest bardzo nieinwazyjne, nie opiera się o stany globalne, polega
głównie na kontraktach, oraz dostarcza bardzo lekkie implementacje InMemory or Void
użyteczne w testach.
API nie jest jednak wystarczające, aby wysłać gdziekolwiek sygnały. Potrzebujemy do tego serializera oraz transportu (do tego służy SDK OpenTelemetry).
Flow dostarcza coś bardzo podobnego,
flow-php/telemetry-otlp-bridge.
Zadaniem tej biblioteki jest:
- Serializacja sygnałów zgodnie z protokołem OTLP
- Przesyłanie sygnałów do OpenTelemetry Collector
flow-php/telemetry-otlp-bridge pozwala serializować sygnały do:
- json
- protobuf - wymaga rozszerzenia protobuf
Sygnały natomiast mogą być przesyłane poprzez:
- curl - wbudowany w pełni asynchroniczny transport
- grcp - wymaga rozszerzenia grcp
- http - wymaga implementacji
psr/http-clientorazpsr/http-factory
Sygnały
Czym są sygnały?
Metryki - liczbowe dane o stanie systemu, np. liczba żądań na sekundę, średni czas odpowiedzi, itp.
Ślady (Traces / Spans) - reprezentują pojedyncze operacje w systemie, wraz z ich atrybutami, czasem trwania, itp.
Pojedynczy Trace to tak naprawdę kolekcja Span'ów, które są ze sobą powiązane, tworząc drzewo operacji.
Ponad to, Trace może być powiązany z metrykami, logami, czy innymi Trace'ami, co pozwala na pełniejszy obraz tego, co dzieje się w systemie.
Logi - tekstowe dane o zdarzeniach w systemie, często zawierające dodatkowe atrybuty, takie jak poziom logowania, czy kontekst zdarzenia.
Auto Instrumentacja
Jedną z głównych zalet korzystania z OpenTelemetry jest auto instrumentacja.
Zgodnie z założeniami, autorzy bibliotek (tacy jak ja) powinni integrować swoje narzędzia z API/SDK OpenTelemetry. Dzięki temu, jeżeli w systemie, który używa takich bibliotek skonfigurujemy serializacje oraz warstwę transportu, automatycznie, bez żadnego wysiłku dostaniemy podstawową telemetrię.
Gdyby społeczność PHP podzielała wizję twórców API/SDK - w niemalże każdym projekcie wystarczyłoby skonfigurować SDK aby uzyskać wgląd w to jak działa system produkcyjnie.
Na ten moment, nie ma jednak ani jednej biblioteki, która byłaby natywnie zintegrowana z OpenTelemetry.
Nie wiem, dlaczego tak jest, środowiska takie jak Java czy .NET mają o wiele lepszą adaptację OpenTelemetry niż PHP.
Być może gdyby SDK OpenTelemetry było bardziej popularne, nie zdecydowałbym się na pisanie własnej biblioteki.
Niemniej jednak, powoli ale skutecznie zacząłem integrować wszystkie biblioteki, które rozwijam z flow-php/telemetry.
Stworzyłem też kilka rozszerzeń do istniejących frameworków/bibliotek - co pozwala na równie skuteczną auto
instrumentację.
Biblioteki z natywną integracją:
flow-php/etlflow-php/filesystemflow-php/postgresql
Rozszerzenia do istniejących bibliotek
flow-php/psr7-telemetry-bridgeflow-php/psr18-telemetry-bridgeflow-php/monolog-telemetry-bridgeflow-php/symfony-http-foundation-telemetry-bridgeflow-php/symfony-telemetry-bundleflow-php/phpunit-telemetry-bridge
Uwaga: Jeżeli rozwijasz swój projekt open source - odezwij się.
Wspólnie znajdziemy możliwie najprostszy sposób na integrację twojego narzędzia z telemetrią.
Możemy to zrobić poprzez natywną integrację lub rozszerzenie.
OTEL Collector
OTEL Collector to rewelacyjne narzędzie, służące jako centralny hub do otrzymywania / przetwarzania /
przesyłania
dalej sygnałów.
Stawiając collector pomiędzy systemem a APM'em (Application Performance Monitoring System) unikamy vendor
lockingu.
Unikamy również zbędnych, blokujących operacji I/O. Nasze sygnały są buforowane w pamięci, a w momencie przepełnienia ustawionego bufora lub zakończenia skryptu są przesyłane do collectora.
Poniżej przykłady konfiguracji collectora z monorepozytorium Flow
APM
Na rynku istnieje bardzo dużo popularnych rozwiązań pozwalających na monitorowanie systemów kompatybilnych z
protokołem OTLP.
Są to narzędzia zarówno płatne, jak i w pełni darmowe, dostępne jako usługi lub open source.
Zanim jednak wybierzemy właściwy APM warto zaprzyjaźnić się z samym protokołem OTLP.
Oczywiście większość APM'ów dostarcza swoje biblioteki, pozwalające na monitorowanie systemów, jednak korzystając z nich, wiążemy się bardzo mocno z konkretnym dostawcą.
Moim (przynajmniej na razie) preferowanym rozwiązaniem do lokalnego developmentu jest Aspire
Aspire jest też skonfigurowany jako APM w monorepozytorium Flow, przykład konfiguracji znajduje się tutaj.
Demo
Przykłady kodu flow-php/telemetry znajdziecie na
https://flow-php.com.
Każdy z nich można uruchomić w przeglądarce za pomocą Playgroundu Flow.
Nie ma jednak lepszej formy poznania narzędzia niż dotknięcie go, dlatego każdego zainteresowanego zachęcam do
postawienia lokalnie Aspire, OTEL Collectora, dodanie flow-php/telemetry
oraz flow-php/telemetry-otlp-bridge.
Potem pozostaje już tylko zacząć emitować sygnały!
Poniżej zrzut ekranu z lokalnego Aspire pokazujący telemetrię zebraną podczas wykonywania testów całego monorepozytorium Flow.