Hlavní navigace

Spring Boot Actuator - sledování stavu aplikace

11. 2. 2022 0:00 Jiří Raška

Článek je pokračováním Spring Boot Actuator – dobré o něm vědět.

Detailní popis vlastností Spring Boot Actuator pro tuto oblast najdete v kapitole: 2.8. Health Information.

Příklady pro tuto kapitolu najdete v jraska1/jv-actuator-examples/example-02.

Teď již víme, co je aplikace zač. Dále by bylo také dobré zjistit, jak aplikace jako celek a případně její komponenty fungují. Pro tento účel je součástí Actuator koncový bod health.

Dále se ještě v rámci povídání o sledování stavu aplikace krátce podívám na koncový bod loggers, kterým je možné řídit úroveň zápisu logů.

Takto by mělo vypadat nastavení konfigurace aplikace application.yaml, aby byly koncové body přístupné:

management:
  endpoints:
    web:
      exposure:
        include:
          - health
          - loggers
  endpoint:
    health:
      show-details: always
      show-components: always

Aby bylo vidět zařazení volitelných komponent do přehledu stavu aplikace, udělal jsem v tomto příkladu rozšíření služeb.

Vycházel jsem z původní implementace služeb A a B. Navíc jsem přidal sledování událostí aplikace s tím, že data o události se zapisují jako JSON objekt do Redis databáze instalované lokálně.

Abych mohl přistoupit k Redis databázi, musím si rozšířit závislosti v Maven projektu o:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Dále jsem doplnit implementace obou služeb A a B o publikování události a také o zápisy do logů (ty se mně budou hodit při prezentaci vlastností koncového bodu loggers).

Jako ukázka bude postačovat implementace jedné služby:

@Service
public class ServiceProviderA implements ServiceProvider<RequestA, ResponseA> {

    private static final Logger logger = LoggerFactory.getLogger(ServiceProviderA.class);

    @Autowired private TokenFactory factory;
    @Autowired private ApplicationEventPublisher publisher;
    @Autowired private Function<Object, String> toJson;

    @Override
    public ResponseA perform(RequestA request) {
        logger.debug("REQUEST:\n{}", toJson.apply(request));

        ResponseA response = factory.tokenInstance(request.getTid(), ResponseA.class);
        response.setCode(ResponseCodeType.OK);
        response.setResult(request.getValue() + new Random().nextInt((int) Math.max(request.getValue() / 2, 10)));

        logger.debug("RESPONSE:\n{}", toJson.apply(response));

        publisher.publishEvent(new ProviderEvent(this, ProviderEvent.Phase.ANSWERED, request, response));

        logger.info("Service Provider name='{}' replied to request from node='{}'.", getInstanceName(), request.getName());
        return response;
    }

    @Override
    public String getInstanceName() {
        return "Provider A";
    }
}

Všechny části by již měly být dostatečné známé, takže je ponechám bez většího komentáře.

Teď bychom se mohli podívat, co nám koncový bod health řekne o aplikaci:

Nastartovat aplikaci pro tento příklad:

[raska@fedora example-02]$ java -jar target/example-02-1.0.0.jar

Dále pak zkusit vyvolat:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/health | jq .
{
  "status": "DOWN",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 210452348928,
        "free": 198957740032,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "redis": {
      "status": "DOWN",
      "details": {
        "error": "org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhost:6379"
      }
    }
  }
}

Je vidět, že v přehledu mně přibyla komponenta redis, ale bohužel má problém. Je způsoben tím, že jsem prozatím databázi nespustil. Pokud to udělám, pak vše již vypadá lépe:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/health | jq .
{
  "status": "UP",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 210452348928,
        "free": 198957740032,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "redis": {
      "status": "UP",
      "details": {
        "version": "6.2.6"
      }
    }
  }
}

Jak zjistit stav vlastních komponent

Zatím jste mohli vidět, že mám ve sledování komponentu redis. Ale já bych chtěl sledovat i své vlastní služby, tedy implementace A a B.

Dá se to udělat opět velice jednoduše. Pokud Spring najde ve svém kontextu nějakou komponentu, která implementuje rozhraní HealthIndicator, pak jí při dotazu na stav vyvolá a výsledek zařadí do celkového přehledu.

Takže jsem si udělal vlastní komponentu pro sledování mých služeb, jedná se o třídu component/ServicesHealthIndicator:

@Component
@Profile("health-indicator")
public class ServicesHealthIndicator implements HealthIndicator {

    @Autowired(required = false) List<ServiceProvider<RequestA, ResponseA>> providersA;
    @Autowired(required = false) List<ServiceProvider<RequestB, ResponseB>> providersB;
    @Autowired private TokenFactory factory;

    @Override
    public Health health() {
        Map<String, Boolean> details = new HashMap<>();

        if (providersA != null)
            for (ServiceProvider<RequestA, ResponseA> provider : providersA) {
                RequestA request = factory.tokenInstance(RequestA.class);
                request.setValue(0);
                ResponseA response = provider.perform(request);

                details.put(provider.getInstanceName(), response.getCode() == ResponseCodeType.OK);
            }

        if (providersB != null) {
            for (ServiceProvider<RequestB, ResponseB> provider : providersB) {
                RequestB request = factory.tokenInstance(RequestB.class);
                request.setText("");
                ResponseB response = provider.perform(request);

                details.put(provider.getInstanceName(), response.getCode() == ResponseCodeType.OK);
            }
        }
        return ((details.values().stream().allMatch(b -> b)) ? Health.up() : Health.down()).withDetails(details).build();
    }
}

Nejdříve jsem si nechal injektovat všechny objekty implementující rozhraní služby A do providersA, a rozhraní služby B do providersB. Opět nevím, jestli při spuštění nějaké takové budou a případně kolik jich bude, takže seznamy mohou obsahovat několik instancí nebo být null (žádné takové nejsou).

Dále abych naplnil požadavek rozhraní HealthIndicator, musím implementovat metodu health().

Pro každé rozhraní vytvořím požadavek, který do něj pošlu a očekávám odpověď. V případě, že dostanu odpověď OK, tak předpokládám, že služba funguje.

Toto je pouze příklad, kdy mohu volně vyvolávat aplikační služby. V reálu tohle asi není úplně vždy možné, ale pak si jistě svůj test vymyslíte jinak.

Výsledek testu zapisují do detailu, který bude připojen k celkovému stavu v rámci návratové hodnoty metody.

Pokud si tuto komponentu při startu povolím, pak bych již měl dostat v přehledu také stav mých dvou implementací.

Takže spustit aplikaci s profilem health-indicator:

[raska@fedora example-02]$ java -jar target/example-02-1.0.0.jar --spring.profiles.active=health-indicator

A následně se můžete podívat, co nám řekne koncový bod health:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/health | jq .
{
  "status": "UP",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 210452348928,
        "free": 198987350016,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "redis": {
      "status": "UP",
      "details": {
        "version": "6.2.6"
      }
    },
    "services": {
      "status": "UP",
      "details": {
        "Provider A": true,
        "Provider B": true
      }
    }
  }
}

Přibyla mně komponenta services se stavem označeným za funkční, a s výsledky testů jednotlivých poskytovatelů služeb.

Název komponenty je odvozen od názvu třídy implementující rozhraní HealthIndicator, což je ServicesHealthIndicator.

Abyste si mohli vyzkoušet jak to vypadá, když nějaká služba nefunguje, tak jsem přidal ještě jednu implementaci služby component/ServiceProviderAFlawed. Ta se spustí při nastavení profilu service-flawed.

Takže ještě jednou spustit aplikaci, tentokrát s profily health-indicatora service-flawed:

[raska@fedora example-02]$ java -jar target/example-02-1.0.0.jar --spring.profiles.active=health-indicator,service-flowed

A výsledek po otestování:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/health | jq .
{
  "status": "DOWN",
  "components": {
    "diskSpace": {
      "status": "UP",
      "details": {
        "total": 210452348928,
        "free": 198957477888,
        "threshold": 10485760,
        "exists": true
      }
    },
    "ping": {
      "status": "UP"
    },
    "redis": {
      "status": "UP",
      "details": {
        "version": "6.2.6"
      }
    },
    "services": {
      "status": "DOWN",
      "details": {
        "Provider A - Flawed": false,
        "Provider A": true,
        "Provider B": true
      }
    }
  }
}

Protože jedna ze služeb nahlásila chybu, celá komponenta je označena jako nefunkční.

Nastavení úrovně logování za běhu aplikace

Pokud se v běhu aplikace objeví nějaký problém, obvykle prvním krokem každého programátora nebo správce je nastavení detailnější úrovně logování s očekáváním, že z těch detailnějších logů bude možné poznat, v čem problém vězí. Jestli je to naivní očekávání či nikoliv tady řešit nebudu. V tomto momentu mne zajímá, jak to udělat.

Nastavení logerů, úroveň logování a formát zpráv je obvykle uděláno v konfiguračním souboru aplikace. Ten je ale schován v JAR balíčku a načítá se při startu. Takže pro jeho změnu bych potřeboval minimálně restart aplikace (ono je možné dodat konfigurační soubor i externě).

Možností, jak nastavit úroveň logování za běhu, je koncový bod loggers.

Můžete si vyzkoušet jej zavolat (toto je opět hodně zkrácený výpis):

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/loggers | jq .
{
  "levels": [
    "OFF",
    "ERROR",
    "WARN",
    "INFO",
    "DEBUG",
    "TRACE"
  ],
  "loggers": {
    "ROOT": {
      "configuredLevel": "INFO",
      "effectiveLevel": "INFO"
    },
    "_org": {
      "configuredLevel": null,
      "effectiveLevel": "INFO"
    },
    "_org.springframework": {
      "configuredLevel": null,
      "effectiveLevel": "INFO"
    },

    "reactor.util": {
      "configuredLevel": null,
      "effectiveLevel": "INFO"
    },
    "reactor.util.Loggers": {
      "configuredLevel": null,
      "effectiveLevel": "INFO"
    }
  },
  "groups": {
    "web": {
      "configuredLevel": null,
      "members": [
        "org.springframework.core.codec",
        "org.springframework.http",
        "org.springframework.web",
        "org.springframework.boot.actuate.endpoint.web",
        "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
      ]
    },
    "sql": {
      "configuredLevel": null,
      "members": [
        "org.springframework.jdbc.core",
        "org.hibernate.SQL",
        "org.jooq.tools.LoggerListener"
      ]
    }
  }
}

Opět se dá grepovat, nebo protože vím, jakou třídu chci detailněji sledovat můžu vybírat i přes jq.

Takže se dále zaměřím na všechny třídy v package component:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/loggers | jq '.loggers."cz.dsw.actuator_examples.example02.component"'
{
  "configuredLevel": null,
  "effectiveLevel": "INFO"
}

Mohu si změnit efektivní úroveň logování:

[raska@fedora ~]$ jo configuredLevel=DEBUG | curl -s -X POST -d @- -H 'Content-Type:application/json' http://localhost:8080/actuator/loggers/cz.dsw.actuator_examples.example02.component

A následně si zkontrolovat výsledek:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/loggers | jq '.loggers."cz.dsw.actuator_examples.example02.component"'
{
  "configuredLevel": "DEBUG",
  "effectiveLevel": "DEBUG"
}

Abych vydráždil službu, zavolám její REST rozhraní:

[raska@fedora ~]$ jo -p nid=local:node02 name=node02 tid=${RANDOM} value=100 | curl -s -X GET --data-binary @- -H "Content-type: application/json" http://localhost:8080/rest/serviceA | jq .
{
  "nid": "local:node01",
  "name": "node01",
  "tid": "14249",
  "ts": "2022-01-14T18:01:45.580+00:00",
  "code": "OK",
  "result": 122
}

A ve výpisu aplikace byste měli vidět něco takového:

2022-01-14 19:01:45.580 DEBUG 5307 --- [nio-8080-exec-6] c.d.a.e.component.ServiceProviderA       : REQUEST:
{
  "nid" : "local:node02",
  "name" : "node02",
  "tid" : "14249",
  "ts" : "2022-01-14T18:01:45.579+00:00",
  "value" : 100
}
2022-01-14 19:01:45.580 DEBUG 5307 --- [nio-8080-exec-6] c.d.a.e.component.ServiceProviderA       : RESPONSE:
{
  "nid" : "local:node01",
  "name" : "node01",
  "tid" : "14249",
  "ts" : "2022-01-14T18:01:45.580+00:00",
  "code" : "OK",
  "result" : 122
}
2022-01-14 19:01:45.580  INFO 5307 --- [nio-8080-exec-6] c.d.a.e.component.ServiceProviderA       : Service Provider name='Provider A' replied to request from node='node02'.

A to je dnes vše. Příště se podívám na metriky aplikace a vytváření custom endpoints.

Sdílet