Na tym blogu zdarza się, iż są poruszane tematy tabu. Tym razem przyszedł czas na statyczne publiczne zmienne. Wiele już zostało powiedziane na temat zła, które wyrządziło światu ich stosowanie. W tym poście chciałbym przeanalizować szczególny przypadek ich użycia, a mianowicie metryki.
Metryki, wszędzie metryki
W poście Mierz logi na zamiary Bartek opisał jak istotne są metryki. Zbieranie miar jest zarazem bardzo proste. W spring framework, wystarczy wstrzyknąć MeterRegistry…
No właśnie, wstrzyknąć MeterRegistry. Czy naprawdę aby zbierać metryki, wszystko w moim kodzie musi być Bean’em? Czy jeśl chcę zbierać metryki w POJO, to muszę zrobić fabrykę, która będzie Bean’em i ustawi MetrerRegistry w instancji POJO?
Oczywiście NIE! Przecież jeżeli chcesz coś zalogować, to nie wstrzykujesz loggera (przynajmniej z tym się nie spotkałem). W zamian używasz statycznej instancji i logujesz co chcesz i gdzie chcesz. Wychodzę z założenia, iż logi i miary nie są od siebie aż tak odległe. Spróbujmy więc zastosować podobne podejście do miar.
Zmienne globalne na ratunek
W najprostszym podejściu moglibyśmy po prostu utworzyć klasę z publicznym statycznym polem przechowującym instancję MeterRegistry. Pole inicjowalibyśmy na starcie aplikacji a następnie dowolnie mogli go używać. Poniżej przedstawiam kilka bardziej rozbudowaną implementację, w której mutator ma widoczność pakietową a akcesor publiczną.
public class MeterRegistryHolder { private static MeterRegistry aMeterRegistry; static void init(MeterRegistry meterRegistry) { aMeterRegistry= meterRegistry; } public static MeterRegistry meterRegistry() { return aMeterRegistry; } } @Configuration public class MeterRegistryHolderInitializer { MeterRegistryHolderInitializer(MeterRegistry meterRegistry) { MeterRegistryHolder.init(meterRegistry); } }Taka implementacja jest bardzo prosta, a zarazem daje ogromną swobodę dodawania metryk w kodzie.
Jeśli potrzebujesz kilku instancji MeterRegistry, bo np. wysyłasz metryki w różne miejsca, możesz rozbudować klasę MeterRegistryHolder tak, aby przechowywała kilka instancji.
Dla wygody możesz dodać statyczny import na MeterRegistryHolder.meterRegistry. Kod nie ulegnie większym zmianom. Zamiast meterRegistry.counter() będzie meterRegistry().counter().
Możemy też skorzystać z klasy Metrics z micrometer. Ma ona znacznie bardziej rozbudowane API dzięki którego, co prawda, nie mamy dostępu do obiektu MetricRegistry ale mamy szereg metod generujących instancje metryk powiązanych z globalnym MetricRegistry.
Ograniczenia
Zmienne globalne nie bez powodu mają złą sławę. Poniżej przedstawiam dwa ograniczenia omawianego podejścia o których warto pamiętać.
Jeśli potrzebujesz testować metryki, możesz to osiągnąć ustawiając Spy lub Mock w MeterRegistryHolder. Zauważ jednak, iż wtedy testy metryk nie powinny być uruchamiane współbieżnie.
Zwróć również uwagę, iż w tej implementacji nie kontrolujemy, w jakiej kolejności są inicjowane beany i kiedy MeterRegistryHolder zostanie zainicjowany. Dlatego, jeżeli próbujesz zbierać metryki w trakcie inicjalizacji kontekstu aplikacji, referencja meterRegistry może być pusta. W takiej sytuacji pozostaje rozbudować MeterRegistryHolder (przygotowałem przykładową implementację na github) lub użycie starego sprawdzonego wstrzykiwanie. Do wszystkiego co dzieje się po zainicjowaniu kontekstu można użyć prostej implementacji MeterRegistryHolder.
Nie zakładam, iż to podejście sprawdzi się we wszystkich przypadkach. W moich ostatnich projektach sprawdziło się świetnie dając dużą swobodę dodawania metryk. jeżeli widzisz jakieś sytuacje, gdzie to podejście całkowicie zawodzi, koniecznie opisz to w komentarzu. Będę bardzo wdzięczny.
Przekaz na dziś
Daj szansę zmiennym globalnym (zwłaszcza gdy zmienność ograniczysz do pakietu).