Spring Boot Actuator - metriky a aplikační koncové body

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

Článek je pokračováním Spring Boot Actuator – sledování stavu aplikace.

Metriky provozu aplikace

Detailní popis vlastností Spring Boot Actuator pro tuto oblast najdete v kapitole: 6. Metrics.

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

Nyní již máme představu z čeho je aplikace poskládaná, v jakém prostředí běží, a nakonec také o tom, jak fungují její části a aplikace jako celek.

Nemáme zatím ale úplně představu, jak je naše aplikace využívaná. Můžeme se podívat do logů a z toho něco odhadovat, ale lepší je zjistit metriky jejího provozu. A právě o tom je tato kapitola.

Spring Boot má pro tento účel zakomponovánu podporu pro napojení na monitorovací systémy – Micrometer. Přes toto rozhraní je možné napojení aplikace na širokou škálu systémů. Podívejte se na výše uvedené odkazy, je jich ke dvaceti.

Napojení na nějaký takový systém ukážu někdy příště. Nyní si vystačím s další podporou zabudovanou rovnou do framework – koncový bod metrics.

S jeho pomocí si můžete kdykoliv zjistit všechny metriky podporované aplikací.

Nejdříve opět povolit koncový bod v konfiguraci application.yaml:

management:
  endpoints:
    web:
      exposure:
        include:
          - metrics
node:
  name: node01
  id: local:${node.name}

A spustit aplikaci:

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

Přehled dostanete, pokud si zavoláte přímo koncový bod:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics | jq .
{
  "names": [
    "application.ready.time",
    "application.started.time",
    "disk.free",
    "disk.total",
    "executor.active",
    "executor.completed",
    "executor.pool.core",
    "executor.pool.max",
    "executor.pool.size",
    "executor.queue.remaining",
    "executor.queued",
    "http.server.requests",
    "jvm.buffer.count",
    "jvm.buffer.memory.used",
    "jvm.buffer.total.capacity",
    "jvm.classes.loaded",
    "jvm.classes.unloaded",
    "jvm.gc.live.data.size",
    "jvm.gc.max.data.size",
    "jvm.gc.memory.allocated",
    "jvm.gc.memory.promoted",
    "jvm.gc.overhead",
    "jvm.gc.pause",
    "jvm.memory.committed",
    "jvm.memory.max",
    "jvm.memory.usage.after.gc",
    "jvm.memory.used",
    "jvm.threads.daemon",
    "jvm.threads.live",
    "jvm.threads.peak",
    "jvm.threads.states",
    "logback.events",
    "process.cpu.usage",
    "process.files.max",
    "process.files.open",
    "process.start.time",
    "process.uptime",
    "system.cpu.count",
    "system.cpu.usage",
    "system.load.average.1m",
    "tomcat.sessions.active.current",
    "tomcat.sessions.active.max",
    "tomcat.sessions.alive.max",
    "tomcat.sessions.created",
    "tomcat.sessions.expired",
    "tomcat.sessions.rejected"
  ]
}

Vidíte, že i v základu jich je docela dost.

Konkrétní metriku získáte tak, že jí přidáte do URL:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/process.cpu.usage | jq .
{
  "name": "process.cpu.usage",
  "description": "The \"recent cpu usage\" for the Java Virtual Machine process",
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 0.0020735771617175864
    }
  ],
  "availableTags": []
}

Nebo pokud se podívám na metriky pro HTTP rozhraní:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/http.server.requests | jq .
{
  "name": "http.server.requests",
  "description": null,
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 13.0
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 0.5736550629999999
    },
    {
      "statistic": "MAX",
      "value": 0.007522524
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "InvalidEndpointBadRequestException",
        "None"
      ]
    },
    {
      "tag": "method",
      "values": [
        "GET"
      ]
    },
    {
      "tag": "uri",
      "values": [
        "/actuator/metrics/{requiredMetricName}",
        "/actuator/metrics"
      ]
    },
    {
      "tag": "outcome",
      "values": [
        "CLIENT_ERROR",
        "SUCCESS"
      ]
    },
    {
      "tag": "status",
      "values": [
        "400",
        "200"
      ]
    }
  ]
}

Je to přece jen delší výpis, ve kterém nám přibylo pole přidělených tagů. Ty nám mohou posloužit pro detailnější rozčlenění metrik.

Tak například, pokud by mne zajímaly HTTP metriky pouze pro dotazy, na které byl návratový kód 400, pak bych mohl hodnotu tagu specifikovat v dotazovacím řetězci:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/http.server.requests?tag=status:400 | jq .
{
  "name": "http.server.requests",
  "description": null,
  "baseUnit": "seconds",
  "measurements": [
    {
      "statistic": "COUNT",
      "value": 1.0
    },
    {
      "statistic": "TOTAL_TIME",
      "value": 0.039342911
    },
    {
      "statistic": "MAX",
      "value": 0.0
    }
  ],
  "availableTags": [
    {
      "tag": "exception",
      "values": [
        "InvalidEndpointBadRequestException"
      ]
    },
    {
      "tag": "method",
      "values": [
        "GET"
      ]
    },
    {
      "tag": "uri",
      "values": [
        "/actuator/metrics/{requiredMetricName}"
      ]
    },
    {
      "tag": "outcome",
      "values": [
        "CLIENT_ERROR"
      ]
    }
  ]
}

Metriky vlastních služeb

Doposud jsem přistupoval pouze k metrikám, které jsou zabodované rovnou do framework. Mohu si ale přidat i vlastní, a to docela jednoduše.

Pokud do Spring kontextu doplním instanci implementující funkční rozhraní MeterBinder, pak je při startu aplikace mezi metriky zařazena nová.

Novou metriku vytvořím pomocí statické metody Gauge.builder(String, Supplier<Number>) a jejím zařazením do registry všech metrik.

Takto vypadá doplněná třída implementující poskytovatele služby A o metriky průměru a směrodatné odchylky návratové hodnoty:

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

    @Autowired private TokenFactory factory;
    private SummaryStatistics stat = new SummaryStatistics();

    @Override
    public ResponseA perform(RequestA request) {
        ResponseA response = factory.tokenInstance(request.getTid(), ResponseA.class);
        response.setCode(ResponseCodeType.OK);
        long value = request.getValue() + new Random().nextInt((int) Math.max(request.getValue() / 2, 10));

        stat.addValue(value);

        response.setResult(value);
        return response;
    }

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

    @Bean
    @Profile("service-metrics")
    public MeterBinder valueMean() {
        return (registry) -> Gauge.builder("services.A.mean", stat::getMean).register(registry);
    }

    @Bean
    @Profile("service-metrics")
    public MeterBinder valueDeviation() {
        return (registry) -> Gauge.builder("services.A.deviation", stat::getStandardDeviation).register(registry);
    }
}

Registrace metrik se provádí pouze pokud máte zadán profil service-metrics (to je volba, aby se mně nepletly při úvodním seznámení s implicitními metrikami framework).

A doplním ještě sledování metrik pro poskytovatele služby B, tentokrát budu sledovat minimum a maximum délky zadaného řetězce:

@Service
public class ServiceProviderB implements ServiceProvider<RequestB, ResponseB> {

    @Autowired private TokenFactory factory;
    private SummaryStatistics stat = new SummaryStatistics();

    @Override
    public ResponseB perform(RequestB request) {
        ResponseB response = factory.tokenInstance(request.getTid(), ResponseB.class);
        response.setCode(ResponseCodeType.OK);
        long length = request.getText().length();

        stat.addValue(length);

        response.setText("text length: " + length);
        return response;
    }

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

    @Bean
    @Profile("service-metrics")
    public MeterBinder valueMinimum() {
        return (registry) -> Gauge.builder("services.B.minimum", stat::getMin).register(registry);
    }

    @Bean
    @Profile("service-metrics")
    public MeterBinder valueMaximum() {
        return (registry) -> Gauge.builder("services.B.maximum", stat::getMax).register(registry);
    }
}

Jak si to vyzkoušet

Vyzkoušení bude chtít trochu více akcí s přípravou dat, aby bylo z těch metrik něco vidět.

Budu potřebovat spustit aplikaci s profilem service-metrics:

[raska@fedora example-03]$ java -jar target/example-03-1.0.0.jar --spring.profiles.active=service-metrics

Nejdříve se můžeme ujistit, že jsou moje metriky registrované:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics | jq .names | grep services
  "services.A.deviation",
  "services.A.mean",
  "services.B.maximum",
  "services.B.minimum",

Takže nejdříve si zkusím službu A trochu více potrápit (tímto budu generovat dotazy):

[raska@fedora ~]$ while true; do jo -p nid=local:node02 name=node02 tid=${RANDOM} value=${RANDOM} | curl -s -X GET --data-binary @- -H "Content-type: application/json" http://localhost:8080/rest/serviceA | jq .result; sleep 1; done

No a pak se mohu podívat na metriky pro službu A:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/services.A.mean | jq .
{
  "name": "services.A.mean",
  "description": null,
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 22222.794871794868
    }
  ],
  "availableTags": []
}
[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/services.A.deviation | jq .
{
  "name": "services.A.deviation",
  "description": null,
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 12942.809162549753
    }
  ],
  "availableTags": []
}

A pro službu B by to mohlo vypadat nějak takto:

[raska@fedora ~]$ for l in $(cat /dev/urandom | tr -dc "\t\n [:alnum:]" | head -n 10); do jo -p nid=local:node03 name=node03 tid=$RANDOM text=$l |  curl -s -X GET --data-binary @- -H "Content-type: application/json" http://localhost:8080/rest/serviceB | jq .text; sleep 1; done

Metriky pak mohu zjistit:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/services.B.minimum | jq .
{
  "name": "services.B.minimum",
  "description": null,
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 1.0
    }
  ],
  "availableTags": []
}
[raska@fedora ~]$ curl -s http://localhost:8080/actuator/metrics/services.B.maximum | jq .
{
  "name": "services.B.maximum",
  "description": null,
  "baseUnit": null,
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 48.0
    }
  ],
  "availableTags": []
}

Zásahy do běžící aplikace

Detailní popis vlastností Spring Boot Actuator pro tuto oblast najdete v kapitole: 2.7. Implementing Custom Endpoints.

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

Poslední oblastí, na kterou bych se rád podíval, je vytváření vlastního koncového bodu Actuator.

Proč bych to měl chtít dělat?

V tomto případě se fantazii meze nekladou. Na druhou stranu nemá asi moc smysl suplovat tím běžné aplikační rozhraní.

Co kdybych ale chtěl měnit chování mé aplikace za běhu, to by asi smysl dávalo. Běžně nastavuji chování aplikace pomocí parametrů zadaných v properties. Pro jejich změnu ale potřebuji minimálně restartovat aplikaci. Přes koncový bod by to mohlo jít i bez restartu.

Jako ukázku jsem si vybral nastavení dvou parametrů margin a divider, které používám při výpočtu návratové hodnoty poskytovatele služby A.

Vytvořím tedy koncový bod application, pomocí kterého bych měl být schopen zobrazit svoje parametry a také je změnit.

Postup je v podstatě jednoduchý a přímočarý.

Nejdříve si musím koncový bod application zařadit do Actuator a povolit přístup k němu, a to nastavením parametrů v application.yaml:

management:
  endpoint:
    application:
      enabled: true
  endpoints:
    web:
      exposure:
        include:
          - application
node:
  name: node01
  id: local:${node.name}

A dále upravím implementaci poskytovatele služby A takto:

@Service
@Endpoint(id = "application")
public class ServiceProviderA implements ServiceProvider<RequestA, ResponseA> {

    @Autowired private TokenFactory factory;

    private long margin = 10;
    private long divider = 2;

    @Override
    public ResponseA perform(RequestA request) {
        ResponseA response = factory.tokenInstance(request.getTid(), ResponseA.class);
        response.setCode(ResponseCodeType.OK);
          long value = new Random().nextInt((int) Math.max(Math.min(request.getValue() / divider, margin), 1));
        response.setResult(value);
        return response;
    }

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

    @ReadOperation
    public Map<String, Long> getData() {
        return Map.of("margin",margin, "divider", divider);
    }

    @WriteOperation
    public void postData(String name, Long value) {
        if (name.equalsIgnoreCase("margin"))
            margin = value;
        else if (name.equalsIgnoreCase("divider"))
            divider = value;
    }
}

Abych mohl vytvořit koncový bod Actuator, musím nějakou službu anotovat jako @Endpoint s uvedením názvu koncového bodu.

Dále jsem rozšířil implementaci o metody pro:

  • čtení parametrů

    Metoda je anotovaná jako @ReadOperation.

    Vrací hodnoty parametrů jako slovník, kde klíčem je název parametru.

  • změnu parametrů

    Metoda anotovaná jako @WriteOperation.

    Jako parametry dostává název parametru a jeho hodnotu.

A to je vše, co musíme udělat.

Nyní si již můžeme spustit aplikaci:

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

A mohu se podívat, co říká Actuator o svých koncových bodech:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator | jq .
{
  "_links": {
    "self": {
      "href": "http://localhost:8080/actuator",
      "templated": false
    },
    "application": {
      "href": "http://localhost:8080/actuator/application",
      "templated": false
    }
  }
}

Je vidět, že zná koncový bod application, tak se na něj můžeme podívat:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/application | jq .
{
  "margin": 10,
  "divider": 2
}

To jsou implicitní hodnoty parametrů, takže zatím to funguje.

A nyní si vyzkouším, jak se projeví jejich změna za běhu. Pustím si průběžné dotazování na službu, budu měnit parametry a sledovat, je se jejich změna projeví na výsledku:

[raska@fedora ~]$ while true; do jo -p nid=local:node02 name=node02 tid=${RANDOM} value=${RANDOM} | curl -s -X GET --data-binary @- -H "Content-type: application/json" http://localhost:8080/rest/serviceA | jq .result; sleep 1; done
6
0
9
7
593
748
513
364
793
2
12
17
0

A souběžně jsem dělal:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/application | jq .
{
  "divider": 2,
  "margin": 10
}
[raska@fedora ~]$ jo name=margin value=1000 | curl -s -X POST -d @- -H "Content-type: application/json" http://localhost:8080/actuator/application | jq .
[raska@fedora ~]$ jo name=divider value=1000 | curl -s -X POST -d @- -H "Content-type: application/json" http://localhost:8080/actuator/application | jq .
[raska@fedora ~]$ curl -s http://localhost:8080/actuator/application | jq .
{
  "divider": 1000,
  "margin": 1000
}

Postupně jsem měnil hodnoty parametrů, a jak je vidět, jejich nastavení se promítlo do výsledků služby.

Sdílet