Przychodzę do Ciebie z ostatnim wpisem na temat Modulith. 5 miesięcy temu poruszyliśmy temat dokumentacji. Wtedy obiecałem, że następnym razem przyjrzymy się metrykom. Muszę jednak przyznać, że ten podmoduł sprawił mi nie lada trudność. Zakładam, że to mój brak doświadczenia w tym temacie przeszkodził mi w doprowadzeniu artykułu do końca. Oczywiście nie tylko ja jestem winny. Magia Springa, w tym ekperymentalnym projekcie, nie ułatwiała sprawy. Dokładniej mówiąc to pokonała mnie konfiguracja. Beany pod maską Springa nie chciały mi się spiąć ze względu na różnice w wersjach biblitotek. Później problemem było brak logowania się metryk. Stąd odstawienie tematu na tak długi okres czasu… Mimo wszystko wróciłem do niego! Dzisiaj zamykamy kwestię opisania na blogu dostępnych funkcjonalności Modulith.

Na ten moment Modulith jest w wersji 0.6.0. Dalej jest to wersja eksperymentalna. Powoli widać światełko w tunelu, aby ten projekt ujrzał światło dzienne. Kilka dni temu wersja została podbita do 1.0.0-SNAPSHOT!

Przedstawienie przykładowego projektu

Zacznijmy od zdefiniowania sobie naprawdę prostego przypadku biznesowego. Nie chcemy przecież zaciemnić sobie głównego wątku artykułu jakim jest Observability w Modulith. Skupimy się tylko i wyłącznie na nim.

Prosta aplikacja na potrzeby przedstawienia Observability w Modulith Prosta aplikacja na potrzeby przedstawienia Observability w Modulith

Prawda, że prosta? Mamy 3 moduły. Jeden z nich, Second, udostępnia pojedynczy enpoint HTTP. Użytkownik może skontaktować się z naszą aplikacją tylko za jego pomocą. Jeśli tego dokona to wysłany zostanie event, na który nasłuchuje moduł First. Następnie on, w tym łańcuszku, bezpośrednio odwołuje się do modułu Third. Nic skomplikowanego.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class Second {

  private final ApplicationEventPublisher publisher;

  Second(ApplicationEventPublisher publisher) {
    this.publisher = publisher;
  }

  public void doSomething() {
    publisher.publishEvent(
        new SecondModuleEvent("second module rulz!"));
  }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class First {

  private static final Log logger = LogFactory.getLog(First.class);

  @Autowired
  private Third third;

  @EventListener
  public void handle(SecondModuleEvent event) {
    logger.info("got message from second module in first module: "
        + event.message());
    third.callFrom("first");
  }
}
1
2
3
4
5
6
7
8
9
@Component
public class Third {

  private static final Log logger = LogFactory.getLog(Third.class);

  public void callFrom(String module) {
    logger.info("called third module from: " + module);
  }
}

Podłączenie modułu Observability do projektu

Przyszła pora na najciekawszą rzecz. Zgodnie z dokumentacją Modulith, aby dodać Observability, należy umieścić w projekcie następującą zależność - org.springframework.experimental:spring-modulith-starter-insight. Jeśli korzystamy z Mavena to wystarczy skopiować kawałek XML, który znajdziemy na wyżej podanej stronie.

1
2
3
4
5
6
<dependency>
  <groupId>org.springframework.experimental</groupId>
  <artifactId>spring-modulith-starter-insight</artifactId>
  <version>{projectVersion}</version>
  <scope>runtime</scope>
</dependency>

W ramach projectVersion mam aktualnie wpisaną wersję 0.5.1 (czyli ówcześnie nie najnowszą). Nie chciałem jej podbijać z racji chęci ustabilizowania aplikacji i braku sił na dalsze boje z konfiguracją.

Na nas ciąży jedynie dodanie dwóch dodatkowych zależności, a właściwie trzech: io.micrometer:micrometer-tracing (wersja 1.0.3), io.micrometer:micrometer-tracing-bridge-brave (wersja 1.1.2) i io.zipkin.reporter2:zipkin-reporter-brave (wersja 2.16.4). Podaje dokładne wersje, ponieważ w tym miejscu napotkałem niewygodny problem. Jedna wersja biblioteki nie chciała współpracować z drugą (brak zgodności klas). Metodą prób i błędów w końcu się udało mi się to rozwiązać. Uczulam Ciebie, żeby się nie poddawać za szybko. Warto spróbować różnych kombinacji wcześniej wspomnianych bibliotek.

Zgodnie z dokumentacją Modulith, aby skonfigurować Tracing, należy zajrzeć do dokumentacji Spring Boota. Tutaj na wstępie twórcy zaznaczają, że Spring Boot dostarcza autokonfigurację dla dwóch dostawców Tracingu: OpenTelemetry with Zipkin, Wavefront, or OTLP lub OpenZipkin Brave with Zipkin or Wavefront. Zdecydowałem się na Zipkin, więc obydwa rozwiązania powinny mi pasować. Wybierając pierwsze z brzegu, czyli OpenTelemetry, napotkałem sporo problemów. Możliwe że coś robiłem po prostu nie do końca poprawnie. Natomiast po zmianie rozwiązania na OpenZipkin Brave wszystko zadziałało natychmiastowo. Jeśli podejmiesz rękawicę to daj znać w komentarzu jak wyglądała sytuacja w Twoim przypadku.

💡 Ważne jest, aby uruchomić również Zipkina. Najlepiej na Dockerze. Instrukcja znajduje się pod tym linkiem.

W mękach przeszliśmy przez podłączenie niezbędnych zależności. Kolejna część jest już o wiele przyjemniejsza. Skorzystamy w niej z dostarczonego “za darmo” rozwiązania. Na pierwszy ogień sprawdźmy Actuator.

Rozszerzone działanie Actuatora

Gdy uda się nam prawidłowo uruchomić aplikację zgodnie z powyższymi instrukcjami, powinniśmy otrzymać dostęp do endpointu /actuator. Wejdźmy zatem przez GET na URL http://localhost:8080/actuator.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "\_links":{
    "self":{
      "href":"http://localhost:8080/actuator",
      "templated":false
    },
    "health":{
      "href":"http://localhost:8080/actuator/health",
      "templated":false
    },
    "health-path":{
      "href":"http://localhost:8080/actuator/health/{*path}",
      "templated":true
    },
    "applicationmodules":{
      "href":"http://localhost:8080/actuator/applicationmodules",
      "templated":false
    }
  }
}

Spring daje nam standardowe endpointy do weryfikacji stanu naszej aplikacji. Natomiast dzięki Modulith dostajemy dodatkowo dostęp do applicationmodules. Wejdźmy zatem w tą ścieżkę i przekonajmy się co się za nią kryje.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
  "thirdmodule":{
    "displayName":"Thirdmodule",
    "basePackage":"io.csanecki.modulith.thirdmodule",
    "dependencies":[
      
    ]
  },
  "secondmodule":{
    "displayName":"Secondmodule",
    "basePackage":"io.csanecki.modulith.secondmodule",
    "dependencies":[
      
    ]
  },
  "firstmodule":{
    "displayName":"Firstmodule",
    "basePackage":"io.csanecki.modulith.firstmodule",
    "dependencies":[
      {
        "types":[
          "EVENT_LISTENER"
        ],
        "target":"secondmodule"
      },
      {
        "types":[
          "USES_COMPONENT"
        ],
        "target":"thirdmodule"
      }
    ]
  }
}

Uzyskujemy wszystkie informacje o tym jakie moduły znajdują się w naszym projekcie oraz w jaki sposób się ze sobą komunikują. Typ zależności EVENT_LISTENER oznacza, że dany moduł nasłuchuje na zdarzenia z innego moduły. Natomiast USES_COMPONENT określa bezpośrednią relację poprzez wywołanie metody beana Springa. Całość jest przejrzysta i podana w przystępnym formacie. Po rozbudowany opis pól zaprezentowanego JSONa zapraszam tutaj.

Nie wiem czy to rozwiązanie nie jest czytelniejsze niż dokumentacja wygenerowana w jednym z poprzednich artykułów. To moja subiektywna opinia. Być może dla kogoś bardziej przyswajalna jest forma graficzna.

Pora na Observability

Acturatora mamy za sobą. Pora teraz na Observability. Żeby je zweryfikować nie trzeba podejmować żadnych dodatkowych akcji poza tymi, które już wykonaliśmy. Wystarczy uderzyć do naszej aplikacji po HTTP, aby odłożyły się logi. Wchodząc lokalnie na Zipkin (http://localhost:9411/zipkin/) powinniśmy je zobaczyć.

Widać, że jakieś logi odłożyły się w Zipkin Widać, że jakieś logi odłożyły się w Zipkin

Śledzenie interakcji pomiędzy modułami w Zipkin Śledzenie interakcji pomiędzy modułami w Zipkin

Observability informuje nas o fakcie wyemitowania zdarzenia przez drugi moduł. Widać, że w trakcie emisji eventu doszło do przetworzenia otrzymanej informacji przez pierwszy moduł oraz interakcji z trzecim modułem. To wszystko możemy zobaczyć bez napisania ani jednej linijki kodu infrastrukturalnego. Wychodzi na to, że Modulith dostarcza nam naprawdę fajne narzędzie do śledzenia ścieżek komunikacyjnych pomiędzy modułami w naszej aplikacji.

💡 Warto ustawić logging.pattern.level=%5p [%X{traceId:-},%X{spanId:-}] w application.properties. W ten sposób poznamy traceId oraz spanId dla danego żądania co ułatwi nam poszukiwania odpowiednich wpisów w Zipkin.

1
2
2023-07-01T22:52:46.741+02:00  INFO [64a0921e7a1644eef5a6922e5699b73a,cc56a0232d29ddf2] 45099 --- [nio-8080-exec-1] io.csanecki.modulith.firstmodule.First   : got message from second module in first module: second module rulz!
2023-07-01T22:52:46.741+02:00  INFO [64a0921e7a1644eef5a6922e5699b73a,38c395399817207f] 45099 --- [nio-8080-exec-1] io.csanecki.modulith.thirdmodule.Third   : called third module from: first

Podsumowanie

Observability jest bardzo istotne z punktu widzenia każdej aplikacji klasy Enterprise. Jeśli dobrze je zaprojektujemy to uzyskamy wiele cennych informacji o tym co się dzieje z naszą aplikacją na danym środowisku. Modulith do tego dodaje od siebie małą ciegiełkę. Pozwala nam na śledzenie tego w jaki sposób współdziałają ze sobą moduły w runtime. I to out of the box! O ile oczywście uda nam się wszystko bezboleśnie skonfigurować…

Tym wpisem chciałbym zakończyć, dawno rozpoczętą, serię o Modulith. Wydaje mi się, że to może być dobre narzędzie do budowania modularnych monolitów. Zobaczymy, w którą stronę pójdzie ten projekt i kiedy dołączy jako pełnoprawny moduł Springa.

To wszystko z mojej strony na dzisiaj. Daj znać w komentarzu jakie masz odczucia co do Modulith.