Spring Boot Actuator - dobré o něm vědět

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

Vážení kolegové programátoři, ruku na srdce. Pro kolik z nás končí svět tím, že commit-ujeme své grandiózní dílo do Gitu. Vždyť dále se to jenom sestaví Jenkinsem, JAR se někam zkopíruje a spustí. On ale ten svět po commit-u je daleko košatější a barvitější, než by na první pohled mohlo vypadat. Pokud se zeptáte kolegů ze support oddělení, budou vám jistě vyprávět hrůzostrašné historky z provozu aplikaci (samozřejmě že ne těch vašich).

Pokud vás to bude zajímat, pokusil bych se podívat na některé aspekty vývoje aplikace z pohledu jejího budoucího provozu.

Takto formulované zadání by bylo jistě hodně široké, takže si vyberu pouze úzkou část. Podívám se na podporu, kterou mně poskytuje framework Spring Boot v oblasti provozu aplikace.

Framework tuto podporu skrývá pod označení Spring Boot Actuator, nebo také „Production-ready Features“.

Jedná se o sadu nástrojů a rozšíření, které by vám měly pomoci při zjišťování informací o běžící aplikaci, stavu aplikace a jejich komponent, využití jejich služeb, monitorování provozu a podobně.

Zdrojové texty ke všem níže uvedeným příkladům najdete v Git repository: jraska1/jv-actuator-examples

Základní informace o Spring Boot Actuator

Framework Spring Boot poskytuje volitelnou podporu pro správu a monitorování aplikace za jejího běhu – Actuator. Jedná se o sadu nástrojů přístupných jako koncové body prostřednictvím rozhraní JMX nebo HTTP.

Vzhledem k tomu, že nejsem velkým příznivcem JMX, zaměřím se v tomto článku pouze na přístup ke koncovým bodům prostřednictvím HTTP. Nicméně je dobré vědět, že tato možnost tady je.

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

Zařazení Actuator do aplikace

Abyste podporu mohli využít, musíte přidat do Maven projektu závislosti:

<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>

To je vše, co potřebujete udělat, aby se vám Actuator přidal do aplikace, inicioval po startu, a poskytl rozhraní přes HTTP.

Pokud si aplikaci spustíte, pak byste měli vidět něco takového:

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

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.1)

2022-01-16 11:38:39.083  INFO 7602 --- [           main] c.d.a.example00.Application              : Starting Application v1.0.0 using Java 11.0.13 on fedora with PID 7602 (/home/raska/IdeaProjects/jv-actuator-examples/example-00/target/example-00-1.0.0.jar started by raska in /home/raska/IdeaProjects/jv-actuator-examples/example-00)
2022-01-16 11:38:39.091  INFO 7602 --- [           main] c.d.a.example00.Application              : No active profile set, falling back to default profiles: default
2022-01-16 11:38:41.182  INFO 7602 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-01-16 11:38:41.206  INFO 7602 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-01-16 11:38:41.207  INFO 7602 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.55]
2022-01-16 11:38:41.336  INFO 7602 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-01-16 11:38:41.337  INFO 7602 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2073 ms
2022-01-16 11:38:42.285  INFO 7602 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 1 endpoint(s) beneath base path '/actuator'
2022-01-16 11:38:42.359  INFO 7602 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-01-16 11:38:42.399  INFO 7602 --- [           main] c.d.a.example00.Application              : Started Application in 4.345 seconds (JVM running for 5.024)
2022-01-16 11:38:42.536  INFO 7602 --- [           main] c.d.a.example00.Application              : *** Hello World, greetings from Dwarf ***

Z výpisu můžete vidět, že se nám něco rozjelo na URL /actuator. To je přístupový bod do HTTP rozhraní Actuator.

Vše, co Actuator umí, poskytuje prostřednictvím tzv. koncových bodů.

Pokud se bavíme s nějakým koncovým bodem Actuator, pak je výsledkem vždy JSON objekt. Proto používám přehlednější formátování výstupu přes nástroj jq.

Nyní si již můžete zkusit s ním popovídat (dostaneme přehled dostupných koncových bodů):

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

Povolení koncových bodů a jejich vystavení

Aby bylo možné využívat služeb koncových bodů, je potřeba povolit jejich spuštění a následně také zpřístupnit je přes HTTP rozhraní.

Implicitní nastavení je takové, že všechny koncové body Actuator jsou při startu povoleny až na jednu výjimku, a tou je shutdown.

Současně však žádný koncový bod není zpřístupněn přes HTTP opět s jednou výjimkou, a tou je health.

Takže si můžete vyzkoušet:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/health | jq .
{
  "status": "UP"
}

Povolení a zpřístupnění koncového bodu se provádí nastavením parametrů v application.yaml:

management.endpoint.<id>.enabled – nastavením parametru na true/false

management.endpoints.web.ex­posure.include – seznamu koncových bodů, které mají být přístupny přes webové rozhraní

Vše bude zřejmější, pokud si ukážeme nastavení pro jednotlivé komponenty dále.

Ještě pro doplnění uvádím, že zde najdete přehled koncových bodů, které jsou k dispozici v rámci frameworku.

Informace o aplikaci zjišťované za běhu

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

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

První oblastí, kde můžete služby Actuator použít, je zjišťování informací o aplikaci za jejího běhu.

Informace o aplikaci

Jedná se o koncový bod info, který poskytuje informace ze čtyř oblastí, a sice:

  • build – informace o sestavení aplikace, zdrojem je META-INF/build-info.properties

    Tento soubor se vám do aplikačního balíku nepřidá sám, musíte v pom.xml doplnit plugin:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>build-info</goal>
            </goals>
        </execution>
    </executions>
</plugin>
  • git – informace o projektu v GIT repository, zdrojem je git.properties

Aby se vám přidal tento soubor do aplikačního balíku, musíte v pom.xml doplnit plugin:

<plugin>
    <groupId>pl.project13.maven</groupId>
    <artifactId>git-commit-id-plugin</artifactId>
</plugin>
  • java – informace o běhovém prostředí Java

  • env – informace o aplikaci specifikované v properties

Poskytuje všechny informace z properties, které jsou ve skupině info.*.

Pokud chceme do těchto parametrů zařadit i informace z pom.xml, pak musíme ještě doplnit resource:

<resource>
    <directory>${basedir}/src/main/resources</directory>
    <filtering>true</filtering>
    <includes>
        <include>**/application*.yml</include>
        <include>**/application*.yaml</include>
        <include>**/application*.properties</include>
    </includes>
</resource>

Všechna potřebná doplnění do pom.xml najdete v příkladech k této kapitole.

Co je potřeba nastavit v application.yaml, aby to fungovalo:

management:
  endpoints:
    web:
      exposure:
        include:
          - info
  info:
    build:
      enabled: true
    env:
      enabled: true
    git:
      enabled: true
    java:
      enabled: true
info:
  app:
    name: "@project.name@"
    description: "@project.description@"
    version: "@project.version@"
    produced-by: "Dwarf's Software Workshop, 2022"
  source:
    target: "@java.version@"
    encoding: "@project.build.sourceEncoding@"

V první části parametrů je povolen koncový bod info. A následně v části parametrů management.info jsou povoleny všechny čtyři oblasti informací.

Nakonec mám sekci parametrů začínajících na info.*. Jejich hodnoty jsou nastaveny při sestavení aplikace z properties definovaných pro Maven nebo rovnou jako text.

Nejdříve spustit aplikaci:

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

A takto vypadá výsledek:

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/info | jq .
{
  "app": {
    "name": "Spring Boot Actuator Example 01",
    "description": "Spring Boot Actuator Examples in Java and Spring Boot - Information about Application",
    "version": "1.0.0",
    "produced-by": "Dwarf's Software Workshop, 2022"
  },
  "source": {
    "target": "11.0.13",
    "encoding": "UTF-8"
  },
  "git": {
    "branch": "main",
    "commit": {
      "id": "fd4f80c",
      "time": "2022-01-11T16:46:31Z"
    }
  },
  "build": {
    "artifact": "example-01",
    "name": "Spring Boot Actuator Example 01",
    "time": "2022-01-11T17:18:31.436Z",
    "version": "1.0.0",
    "group": "cz.dsw.actuator-examples"
  },
  "java": {
    "vendor": "Red Hat, Inc.",
    "version": "11.0.13",
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "11.0.13+8"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Red Hat, Inc.",
      "version": "11.0.13+8"
    }
  }
}

Je ještě jedna možnost, na kterou bych rád upozornil. Vzhledem k tomu, že oblast env načítá informace z properties, můžeme je také nastavit při spuštění aplikace. To se může hodit, pokud si nějak chceme označit jednotlivé instance spuštění, třeba takto:

[raska@fedora example-01]$ java -jar target/example-01-1.0.0.jar --info.instance.id=inst-1234 --info.instance.name=my-application

No a pak informace o běžící instanci dostaneme přes koncový bod info (zkrácený výpis):

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/info | jq .
{
  "instance": {
    "name": "my-application",
    "id": "inst-1234"
  },
  "app": {
    "name": "Spring Boot Actuator Example 01",
    "description": "Spring Boot Actuator Examples in Java and Spring Boot - Information about Application",
    "version": "1.0.0",
    "produced-by": "Dwarf's Software Workshop, 2022"
  },
...
}

Přehled prostředí

Dalším zdrojem informací o běžící aplikaci, je koncový bod env.

Aby byl přístupný, tak toto musí být nastaveno v application.yaml:

management:
  endpoints:
    web:
      exposure:
        include:
          - env

Po jeho vyvolání dostanete přehled nastavení proměnných prostředí (je to hodně dlouhý přehled, proto jsem některé sekce zkrátil tak abyste měli přehled):

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/env | jq .
{
  "activeProfiles": [],
  "propertySources": [
    {
      "name": "server.ports",
      "properties": {
        "local.server.port": {
          "value": 8080
        }
      }
    },
    {
      "name": "commandLineArgs",
      "properties": {
        "info.instance.name": {
          "value": "my-application"
        },
        "info.instance.id": {
          "value": "inst-1234"
        }
      }
    },
    {
      "name": "servletContextInitParams",
      "properties": {}
    },
    {
      "name": "systemProperties",
      "properties": {
        "sun.desktop": {
          "value": "gnome"
        },
        "awt.toolkit": {
          "value": "sun.awt.X11.XToolkit"
        },

...

        "sun.io.unicode.encoding": {
          "value": "UnicodeLittle"
        },
        "java.class.version": {
          "value": "55.0"
        }
      }
    },
    {
      "name": "systemEnvironment",
      "properties": {
        "PATH": {
          "value": "/home/raska/.local/bin:/home/raska/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin",
          "origin": "System Environment Property \"PATH\""
        },
        "XAUTHORITY": {
          "value": "/run/user/1000/.mutter-Xwaylandauth.ODRPF1",
          "origin": "System Environment Property \"XAUTHORITY\""
        },

...

        "SHLVL": {
          "value": "1",
          "origin": "System Environment Property \"SHLVL\""
        },
        "HOME": {
          "value": "/home/raska",
          "origin": "System Environment Property \"HOME\""
        }
      }
    },
    {
      "name": "Config resource 'class path resource [application.yaml]' via location 'optional:classpath:/'",
      "properties": {
        "management.endpoints.web.exposure.include[0]": {
          "value": "info",
          "origin": "class path resource [application.yaml] from example-01-1.0.0.jar - 7:13"
        },
        "management.endpoints.web.exposure.include[1]": {
          "value": "env",
          "origin": "class path resource [application.yaml] from example-01-1.0.0.jar - 8:13"
        },

...

        "node.name": {
          "value": "node01",
          "origin": "class path resource [application.yaml] from example-01-1.0.0.jar - 31:9"
        },
        "node.id": {
          "value": "local:node01",
          "origin": "class path resource [application.yaml] from example-01-1.0.0.jar - 32:7"
        }
      }
    }
  ]
}

Seznam objektů ve Spring Context

Posledním zdrojem informací, který by se vám jako programátorům mohl hodit, je přehled instancí ve Spring Context po spuštění aplikace – koncový bod beans.

Zde si můžete ověřit, zda framework po startu obsahuje instance tříd, které potřebujete a můžete je tedy odkazovat přes @Autowired.

Aby byl přístupný, tak toto musí být nastaveno v application.yaml:

management:
  endpoints:
    web:
      exposure:
        include:
          - beans

A takto to vypadá, pokud si jej zavoláte (opět zkrácený výpis pro přehlednost):

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/beans | jq .
{
  "contexts": {
    "application": {
      "beans": {
        "endpointCachingOperationInvokerAdvisor": {
          "aliases": [],
          "scope": "singleton",
          "type": "org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor",
          "resource": "class path resource [org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.class]",
          "dependencies": [
            "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration",
            "environment"
          ]
        },
        "defaultServletHandlerMapping": {
          "aliases": [],
          "scope": "singleton",
          "type": "org.springframework.web.servlet.HandlerMapping",
          "resource": "class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]",
          "dependencies": [
            "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$EnableWebMvcConfiguration"
          ]
        },
        "applicationTaskExecutor": {
          "aliases": [
            "taskExecutor"
          ],
          "scope": "singleton",
          "type": "org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor",
          "resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.class]",
          "dependencies": [
            "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration",
            "taskExecutorBuilder"
          ]
        },

...

        "application": {
          "aliases": [],
          "scope": "singleton",
          "type": "cz.dsw.actuator_examples.example01.Application$$EnhancerBySpringCGLIB$$c68d1643",
          "resource": null,
          "dependencies": []
        },
        "taskExecutorBuilder": {
          "aliases": [],
          "scope": "singleton",
          "type": "org.springframework.boot.task.TaskExecutorBuilder",
          "resource": "class path resource [org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.class]",
          "dependencies": [
            "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration",
            "spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties"
          ]
        }
      },
      "parentId": null
    }
  }
}

Dohledáte zde nejenom jméno instance a třídu, která jí implementuje, ale také její závislosti na dalších instancích. Přehled je dost velký, ale grep je mocný nástroj, takže není třeba se toho bát.

Vytváření informací za běhu

Prozatím jsem doloval informace o aplikaci z jejího okolí, ať již to bylo nastavení prostředí, Java virtuálního stroje, properties a podobně.

Mám ale ještě jednu možnost, a tou je generování informací rovnou z aplikačního kódu.

Je to docela jednoduché. Každá instance třídy registrovaná ve Spring Context, která implementuje rozhraní InfoContributor, přidává informace do koncového bodu info.

Abych mohl něco ukázat, tak jsem si převzal implementace aplikačních služeb, které jsem používal v článcích o aplikačních událostech (ony jsou triviální, takže je popisovat nebudu, bude jistě stačit krátký pohled do zdrojových kódů).

To, co jsem ale doplnil navíc, je komponenta:

@Component
@Profile("service-info")
public class ServiceInfo implements InfoContributor {

    @Autowired(required = false) List<ServiceProvider> allProviders;

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("service-providers", (allProviders != null) ? allProviders.stream().map(ServiceProvider::getInstanceName).collect(Collectors.toList()) : Collections.emptyList());
    }
}

Je sice krátká, ale jistě bude stát za komentář:

  1. Atribut allProviders je injektován z kontextu Spring jako seznam objektů implementujících rozhraní ServiceProvider. Takových poskytovatelů služeb tam může být několik (nebo také žádný).

  2. Implementace rozhraní InfoContributor vyžaduje implementaci metody contribute(Info.Builder).

    Pokud mám neprázdný seznam poskytovatelů, vytvořím informaci s označením „service-providers“, ve které jsou názvy instancí poskytovatelů služby.

  3. Komponenta se startuje, pokud mám aktivován profil service-info.

Takže si vyvolám aplikaci s aktivním profilem service-info:

[raska@fedora example-01]$ java -jar target/example-01-1.0.0.jar --info.instance.id=inst-1234 --info.instance.name=my-application --spring.profiles.active=service-info

Pokud se nyní podívám na koncový bod info, pak bych měl vidět něco takového (výpis opět zkrácen):

[raska@fedora ~]$ curl -s http://localhost:8080/actuator/info | jq .
{
  "instance": {
    "name": "my-application",
    "id": "inst-1234"
  },
  "app": {
    "name": "Spring Boot Actuator Example 01",
    "description": "Spring Boot Actuator Examples in Java and Spring Boot - Information about Application",
    "version": "1.0.0",
    "produced-by": "Dwarf's Software Workshop, 2022"
  },

...

  "service-providers": [
    "Provider B",
    "Provider A"
  ]
}

Berte to prosím jako jednoduchý příklad. Využití pro vaše potřeby je potřeba vždy zvážit.

A to je dnes vše. Příště se podívám na sledování stavu aplikace a jednotlivých komponent.

Sdílet