A 07-01 vam obrir CicloUrbana per dins amb Actuator: sondes de salut, informació de versió, nivells de log en calent, un endpoint propi i una cadena de seguretat que exigeix ADMIN al port 8081. Vam tancar aquella lliçó deixant deliberadament un endpoint sense obrir, /actuator/metrics, amb una promesa: la instrumentació quedava muntada i aquí es llegiria.
Ha arribat el moment, i amb una necessitat concreta al darrere. A 09-01 vam ajustar el rendiment a base de proves de càrrega i EXPLAIN ANALYZE, és a dir, mirant una vegada i en un entorn controlat. A 09-02 vam muntar memòries cau la utilitat de les quals depèn completament d'una taxa d'encerts que encara ningú no observa. Les dues lliçons van deixar el mateix deute: CicloUrbana no es mesura sola, contínuament i en producció.
Aquesta lliçó el salda. No repetirem què és Actuator ni com s'exposa: farem servir la infraestructura ja muntada per instrumentar de debò. Veurem Micrometer com a façana de mètriques, els cinc tipus de mesurador i quan fer servir cadascun, el catàleg de mètriques que existeixen sense escriure una línia, la regla d'or de la cardinalitat —la que separa un sistema de monitoratge sa d'un que cau pel seu propi pes—, les mètriques de negoci de la xarxa de Ribalta, la diferència crucial entre percentils calculats al client i histogrames agregables, i el model unificat d'observació de Spring Boot 3 que produeix alhora mètrica i traça.
Contingut
- Els tres pilars de l'observabilitat
- Micrometer: l'SLF4J de les mètriques
- Els tipus de mesurador
- Les mètriques que ja existeixen sense escriure codi
- Consultar mètriques des de
/actuator/metrics - Etiquetes i la regla d'or de la cardinalitat
- Instrumentar CicloUrbana:
MetriquesCicloUrbana - El
Gauge, la referència feble i el seu error clàssic @Timedi@Counted- Percentils davant d'histogrames
MeterFilteriMeterRegistryCustomizer- El pool, la base de dades i la memòria cau: quins números vigilar
- L'Observation API: una instrumentació, dues senyals
- De les mètriques als SLO: RED i USE
- Provar que una mètrica es registra
- Errors Comuns i Consells
- Exercicis
- Els tres pilars de l'observabilitat
Monitorar és vigilar un conjunt conegut d'indicadors; observar és poder respondre preguntes que no havies previst. La diferència es nota a les tres de la matinada, quan el problema mai no és el que esperaves. Per aconseguir-ho calen tres senyals, i cap no substitueix les altres:
| Mètriques | Logs | Traces | |
|---|---|---|---|
| Què són | Números agregats en el temps | Esdeveniments amb text i context | El recorregut d'una petició |
| Què responen | Està passant alguna cosa? Quant? Des de quan? | Què va passar exactament? | On se'n va anar el temps? |
| Cost | Molt baix i constant amb el trànsit | Proporcional al trànsit; car | Alt: es mostreja |
| Cardinalitat | Ha de ser baixa | Alta: cada línia és única | Alta per definició |
| Retenció típica | Mesos o anys | Dies o setmanes | Dies |
| Serveixen per alertar | Sí, és la seva funció | Malament (excepte taxes d'error) | No |
| En aquest mòdul | Aquesta lliçó i 09-04 | 09-05 | 09-06 |
El flux de treball real d'una incidència a Ribalta fa servir les tres en aquest ordre: una alerta basada en mètriques avisa que el p99 dels lloguers s'ha disparat; una traça mostra que el 80 % del temps és al span de la passarel·la de pagaments; els logs d'aquella traça concreta, filtrats pel seu identificador, diuen quin error va retornar. Mètriques per detectar, traces per localitzar, logs per entendre.
Aquesta lliçó construeix el primer pilar dins de l'aplicació. A 09-04 aquestes mètriques sortiran del procés cap a Prometheus i Grafana, perquè —i convé dir-ho ja— tot el que instrumentem aquí viu en memòria i mor amb el procés.
- Micrometer: l'SLF4J de les mètriques
La comparació és literal i explica el disseny sencer. Igual que SLF4J et deixa escriure log.info(...) sense saber si al darrere hi ha Logback o Log4j2, Micrometer et deixa registrar un comptador sense saber si al darrere hi ha Prometheus, Datadog, New Relic o CloudWatch. El teu codi depèn d'una façana; el sistema de destinació és una dependència intercanviable.
flowchart LR
A[Codi de CicloUrbana<br/>Counter, Timer, Gauge] --> B[MeterRegistry<br/>Micrometer]
C[Spring Boot<br/>metriques automatiques] --> B
B --> D[Actuator<br/>/actuator/metrics]
B --> E[PrometheusMeterRegistry<br/>/actuator/prometheus]
E --> F[(Prometheus)]
F --> G[Grafana · 09-04]
MeterRegistry és la peça central: la fàbrica i el registre de tots els mesuradors. Spring Boot en crea un automàticament en detectar Actuator, i s'injecta com qualsevol altre bean. Sense cap implementació concreta al classpath es fa servir un SimpleMeterRegistry en memòria, que és justament el que fa útil /actuator/metrics en desenvolupament i en proves (apartat 15).
Un detall important de vocabulari: a Micrometer un mesurador (meter) és l'abstracció genèrica, i Counter, Gauge, Timer, DistributionSummary i LongTaskTimer en són els tipus. Cada mesurador s'identifica pel seu nom més el seu conjunt d'etiquetes, i cada combinació diferent és una sèrie temporal independent. Aquesta frase és la clau de l'apartat 6.
- Els tipus de mesurador
| Tipus | Què mesura | Pot baixar | Exemple a CicloUrbana |
|---|---|---|---|
Counter |
Un valor acumulat que només puja | No | Lloguers iniciats, errors de cobrament |
Gauge |
Un valor instantani que puja i baixa | Sí | Bicicletes disponibles, mida d'una cua |
Timer |
Durada i recompte d'esdeveniments curts | — | Temps del càlcul de tarifa, latència HTTP |
DistributionSummary |
Distribució de valors sense unitat de temps | — | Durada en minuts d'un lloguer, import |
LongTaskTimer |
Durada de tasques llargues en curs | — | La importació nocturna de l'ajuntament |
FunctionCounter |
Comptador llegit d'un objecte extern | No | Un total que ja porta una altra biblioteca |
Quatre criteris d'elecció que resolen gairebé tots els dubtes:
Counter davant de Gauge. Si la pregunta és «quantes vegades ha passat?», és un comptador; si és «quant n'hi ha ara mateix?», és un indicador. Un comptador mai no es decrementa: per a «lloguers en curs» no es fa servir un comptador que puja en iniciar i baixa en finalitzar —això és un Gauge—, perquè el valor d'un comptador és el seu ritme de creixement, que és el que rate() sabrà explotar a 09-04.
Timer davant de DistributionSummary. El Timer mesura temps i porta unitats; el DistributionSummary mesura qualsevol magnitud. La durada d'un trajecte de bicicleta en minuts sembla temps, però no és la durada d'una operació del programa: és una dada de negoci, i va en un DistributionSummary.
Timer davant de LongTaskTimer. Un Timer només registra l'operació quan acaba: si una importació triga quaranta minuts, durant aquests quaranta minuts el Timer no diu res. El LongTaskTimer informa de les tasques actives i de quant fa que duren, que és justament el que es vol vigilar d'una tasca @Scheduled de 07-03.
Tot Timer és també un comptador. Publica _count, _sum i _max, així que temporitzar una operació dóna de franc el nombre de vegades que va passar: no cal un Counter a part.
- Les mètriques que ja existeixen sense escriure codi
Abans d'instrumentar res, convé saber què hi ha. Amb Actuator i les autoconfiguracions actives, CicloUrbana ja publica:
| Mètrica | Tipus | Què mesura | Per què importa |
|---|---|---|---|
http.server.requests |
Timer | Latència i recompte per uri, method, status, outcome, exception |
La mètrica més valuosa del sistema: RED complet de l'apartat 14 |
http.client.requests |
Timer | El mateix, per al RestClient cap a la passarel·la (07-06) |
Separa «som lents» de «el remot és lent» |
jvm.memory.used / .max |
Gauge | Memòria per àrea (heap, nonheap) i per pool |
La memòria que puja i no baixa de 09-01 |
jvm.gc.pause |
Timer | Durada de les pauses de GC, per causa | Explica pics de p99 |
jvm.threads.live / .states |
Gauge | Fils vius i el seu estat | Fils bloquejats esperant connexió |
hikaricp.connections.* |
Gauge/Timer | active, idle, pending, acquire, usage, timeout |
El senyal de l'apartat 11 de 09-01 |
system.cpu.usage / process.cpu.usage |
Gauge | CPU de la màquina i del procés | Saturació |
tomcat.threads.busy / .config.max |
Gauge | Fils del contenidor web ocupats | Quant marge queda |
spring.data.repository.invocations |
Timer | Crides a cada mètode de repositori | Quina consulta domina el temps |
cache.gets / cache.puts / cache.evictions / cache.size |
Counter/Gauge | Encerts i fallades per memòria cau (09-02) | Si la memòria cau serveix d'alguna cosa |
resilience4j.circuitbreaker.* |
Gauge/Timer | Estat i crides del tallacircuits (07-06) | Circuit obert = degradació |
application.ready.time / started.time |
Gauge | Quant va trigar a arrencar | Regressions d'arrencada a Kubernetes |
logback.events |
Counter | Esdeveniments de log per nivell | Un pic de level="error" és una alerta barata |
Dues observacions. La primera: http.server.requests tota sola respon a gairebé tot, perquè té recompte (trànsit), etiqueta status (errors) i distribució (latència), que són les tres senyals del mètode RED. La segona: spring.data.repository.invocations requereix activar-se (management.metrics.data.repository.autotime.enabled: true) i és el complement natural del comptador de consultes de 09-01, ara en producció i de manera contínua.
- Consultar mètriques des de
/actuator/metrics
/actuator/metricsRecordant la configuració de 07-01 —port de gestió 8081 i ADMIN per httpBasic—, n'hi ha prou d'afegir l'endpoint a la llista exposada:
management:
endpoints.web.exposure.include: health,info,loggers,metrics,caches,prometheus
metrics.tags: # etiquetes comunes a TOTES les metriques
application: ciclourbana
entorn: ${SPRING_PROFILES_ACTIVE:local}
instancia: ${HOSTNAME:local}Aquestes tres etiquetes comunes són la diferència entre «la latència és alta» i «la latència és alta a la instància 3 de producció». S'apliquen a tots els mesuradors, inclosos els automàtics.
curl -su admin:*** http://localhost:8081/actuator/metrics # el cataleg
curl -su admin:*** http://localhost:8081/actuator/metrics/http.server.requests{
"name": "http.server.requests",
"measurements": [
{ "statistic": "COUNT", "value": 128431 },
{ "statistic": "TOTAL_TIME", "value": 4021.83 },
{ "statistic": "MAX", "value": 2.41 }
],
"availableTags": [
{ "tag": "uri", "values": ["/api/v1/estacions", "/api/v1/lloguers", "/api/v1/lloguers/{id}/finalitzar"] },
{ "tag": "status", "values": ["200", "201", "404", "409", "500"] },
{ "tag": "outcome", "values": ["SUCCESS", "CLIENT_ERROR", "SERVER_ERROR"] }
]
}El que importa és com es llegeix: TOTAL_TIME / COUNT és la latència mitjana —4021,83 s entre 128.431 peticions = 31 ms—, i ja sabem de 09-01 el poc que val una mitjana. Per acostar-se al que importa es filtra amb ?tag=, encadenant-ne diversos:
# Latencia i recompte nomes de l'endpoint de lloguers
.../metrics/http.server.requests?tag=uri:/api/v1/lloguers
# Nomes els errors de servidor d'aquest endpoint
.../metrics/http.server.requests?tag=uri:/api/v1/lloguers&tag=outcome:SERVER_ERRORFixa't en l'etiqueta uri: diu /api/v1/lloguers/{id}/finalitzar, amb la plantilla i no amb l'identificador real. Aquesta decisió de Spring és la que fa utilitzable la mètrica, i és exactament el tema de l'apartat següent.
I una limitació que convé interioritzar: aquest endpoint dóna el valor acumulat ara. No hi ha història, no hi ha gràfic, no hi ha «fa dues hores». Serveix per comprovar que una mètrica existeix i per a una consulta puntual d'urgència; per a tota la resta cal 09-04.
- Etiquetes i la regla d'or de la cardinalitat
Una etiqueta (tag, o label a Prometheus) és una dimensió: permet desglossar lloguers.iniciats per tipus de tarifa o http.server.requests per endpoint. Sense etiquetes, una mètrica només diu «passen coses»; amb elles, diu quines coses.
Però cada combinació diferent de nom i valors d'etiqueta és una sèrie temporal independent, amb la seva pròpia memòria a l'aplicació, la seva pròpia sèrie a Prometheus i el seu propi cost a cada consulta. D'aquí la regla d'or:
La cardinalitat d'una etiqueta ha de ser baixa i acotada. Si el nombre de valors possibles creix amb el nombre d'usuaris, de peticions o de files, no és una etiqueta.
Aplicat a CicloUrbana:
| Etiqueta | Valors possibles | Correcta? | Motiu |
|---|---|---|---|
tipusTarifa |
3 | Sí | Fix i conegut |
estacio |
~40 | Sí | Acotat i creix molt a poc a poc |
estat del lloguer |
4 | Sí | Fix |
uri amb plantilla |
~25 endpoints | Sí | Acotat pel codi |
instancia |
3-10 | Sí | Acotat pel desplegament |
idUsuari |
Desenes de milers | No | Explosió: una sèrie per ciutadà |
uri sense plantilla |
Infinits (/lloguers/48213) |
No | Una sèrie per lloguer |
rastreId |
Un per petició | Mai | Cardinalitat infinita per definició |
| Missatge d'excepció | Il·limitat | No | Pot portar dades variables |
Què passa exactament quan s'incompleix. Suposem Counter.builder("lloguers.iniciats").tag("usuari", idUsuari). Amb 50.000 ciutadans apareixen 50.000 sèries: l'aplicació reté 50.000 objectes mesurador en memòria i el seu MeterRegistry creix sense parar; /actuator/prometheus passa de retornar 200 KB a retornar desenes de megabytes a cada recollida; Prometheus multiplica el seu consum de memòria i índex; i les consultes de Grafana es tornen lentes o directament fallen. És la causa número u de caigudes de sistemes de monitoratge, té nom propi —cardinality explosion— i el pitjor és que no falla el dia que es desplega, sinó tres setmanes després, quan ja ningú no relaciona una cosa amb l'altra.
La regla pràctica que evita l'error: si una dada identifica un individu o una petició concreta, va en un log (09-05) o en una traça (09-06), mai en una etiqueta de mètrica. Les mètriques responen «quants» i «quant»; el «quin» és feina de les altres dues senyals.
Un cas intermedi que convé conèixer: a Ribalta, 40 estacions són una etiqueta perfectament sana. Si CicloUrbana s'estengués a una xarxa metropolitana de 4.000 estacions caldria revisar la decisió —4.000 sèries per mètrica multiplicades per cada mètrica d'estació comença a ser molt— i probablement passar a etiquetar per districte i deixar l'estació concreta per a les traces.
- Instrumentar CicloUrbana:
MetriquesCicloUrbana
MetriquesCicloUrbanaLes mètriques automàtiques diuen si el sistema està sa; les de negoci diuen si el servei està sa. Són preguntes diferents: una API que respon 200 en 30 ms amb zero lloguers iniciats a les vuit del matí està tècnicament perfecta i funcionalment trencada.
Tot es concentra en un component, que és la pràctica que envelleix millor:
package com.ciclourbana.comu.metriques;
@Component
public class MetriquesCicloUrbana {
private final MeterRegistry registre;
private final Timer temporitzadorTarifa;
private final DistributionSummary duradaLloguers;
// La referencia FORTA que mante vius els gauges (apartat 8)
private final Map<Long, AtomicInteger> disponiblesPerEstacio = new ConcurrentHashMap<>();
public MetriquesCicloUrbana(MeterRegistry registre) {
this.registre = registre;
this.temporitzadorTarifa = Timer.builder("ciclourbana.tarifa.calcul")
.description("Temps de calcul de l'import d'un lloguer")
.publishPercentileHistogram() // apartat 10
.register(registre);
this.duradaLloguers = DistributionSummary.builder("ciclourbana.lloguer.durada")
.description("Durada dels lloguers finalitzats")
.baseUnit("minuts")
.publishPercentileHistogram()
.register(registre);
}
/** Comptador amb etiqueta de baixa cardinalitat: 3 valors possibles. */
public void lloguerIniciat(TipusTarifa tarifa, Long idEstacio) {
Counter.builder("ciclourbana.lloguers.iniciats")
.description("Lloguers iniciats")
.tag("tarifa", tarifa.name())
.tag("estacio", String.valueOf(idEstacio))
.register(registre)
.increment();
}
public void lloguerFinalitzat(TipusTarifa tarifa, Duration durada) {
Counter.builder("ciclourbana.lloguers.finalitzats")
.tag("tarifa", tarifa.name())
.register(registre)
.increment();
duradaLloguers.record(durada.toMinutes());
}
/** Timer que embolcalla el calcul: mesura i retorna el resultat. */
public BigDecimal medirCalculTarifa(Supplier<BigDecimal> calcul) {
return temporitzadorTarifa.record(calcul);
}
/** Gauge per estacio: es registra una vegada i despres nomes s'actualitza el valor. */
public void actualitzarDisponibles(Long idEstacio, String nom, int disponibles) {
disponiblesPerEstacio.computeIfAbsent(idEstacio, id -> {
AtomicInteger valor = new AtomicInteger();
Gauge.builder("ciclourbana.bicicletes.disponibles", valor, AtomicInteger::get)
.description("Bicicletes disponibles per estacio")
.tag("estacio", nom)
.register(registre);
return valor;
}).set(disponibles);
}
}I el seu ús des del servei de domini, que no coneix Micrometer:
@Transactional
public LloguerResponse iniciar(IniciarLloguerRequest peticio) {
Lloguer lloguer = /* ... logica de domini ... */;
BigDecimal estimat = metriques.medirCalculTarifa(
() -> calculadora.estimar(peticio.tipusTarifa(), DURADA_MITJANA));
metriques.lloguerIniciat(peticio.tipusTarifa(), peticio.estacioOrigenId());
return mapejador.aResposta(lloguer);
}Tres decisions de disseny mereixen comentari. Un sol component concentra els noms de les mètriques: sense ell, les cadenes "ciclourbana.lloguers.iniciats" es dispersen pel codi i apareixen les variants (lloguers_iniciats, lloguer.iniciat) que trenquen els quadres de comandament. Els noms segueixen la convenció de Micrometer: minúscules separades per punts i jerarquia del general al particular (ciclourbana.lloguers.iniciats), que cada sistema tradueix al seu propi estil —Prometheus ho convertirà a ciclourbana_lloguers_iniciats_total—. I la instrumentació no canvia el comportament: si registre fos un SimpleMeterRegistry de proves, tot continua funcionant igual.
Un advertiment sobre el Counter dins del mètode: Counter.builder(...).register(registre) a cada invocació no crea un comptador nou, perquè register retorna l'existent si el nom i les etiquetes coincideixen. És correcte, tot i que té un petit cost de cerca; en un camí molt calent convé desar els comptadors en un Map per etiqueta.
- El
Gauge, la referència feble i el seu error clàssic
Gauge, la referència feble i el seu error clàssicUn Gauge no emmagatzema valors: desa una referència a l'objecte que els té i una funció per llegir-lo, i aquesta referència és feble. Micrometer ho fa expressament, per no impedir que el recol·lector d'escombraries alliberi objectes que l'aplicació ja no fa servir: una mètrica mai no ha de provocar una fuita de memòria.
La conseqüència és l'error més desconcertant de tot Micrometer:
// MALAMENT: l'AtomicInteger no te cap referencia forta
public void publicarDisponibles(Long idEstacio, int valor) {
Gauge.builder("ciclourbana.bicicletes.disponibles", new AtomicInteger(valor),
AtomicInteger::get)
.tag("estacio", String.valueOf(idEstacio))
.register(registre);
}Això funciona perfectament durant minuts i després la mètrica comença a retornar NaN. Quan el GC passa, l'AtomicInteger —al qual ningú no apunta llevat de la referència feble del gauge— desapareix, i el mesurador es queda sense font. I la fallada és intermitent i depèn de la pressió de memòria, cosa que la converteix en el clàssic «en desenvolupament funcionava».
La solució és la de l'apartat 7: el Map de camp manté la referència forta mentre el component visqui. Dues altres formes correctes: fer servir registre.gauge("nom", tags, objecteQueJaViu, funcio) sobre un objecte de llarga vida, o Gauge.builder("cua.pendents", laCua, Collection::size) sobre una col·lecció que ja és un camp del bean.
Tres regles més sobre gauges. Es registra una vegada i després només s'actualitza el valor: tornar a registrar el mateix nom amb les mateixes etiquetes retorna l'existent, però registrar-lo dins d'un bucle és una olor de disseny. La funció s'invoca en el moment de la recollida, no quan tu la crides: per això ha de ser barata i no ha de llançar excepcions —mai un Gauge que executi una consulta a la base de dades, perquè Prometheus la llançaria cada 15 segons—. I un gauge que no canvia mai no aporta res: és una constant disfressada de mètrica.
Per a la disponibilitat de Ribalta, el gauge es refresca des de la tasca programada de 07-03, que ja recorre les estacions cada quinze segons, en lloc de calcular-se a cada recollida.
@Timed i @Counted
@Timed i @CountedPer instrumentar sense escriure codi, Micrometer ofereix dues anotacions basades en aspectes. Requereixen la dependència d'AOP i registrar els aspectes:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>@Bean TimedAspect timedAspect(MeterRegistry registre) { return new TimedAspect(registre); }
@Bean CountedAspect countedAspect(MeterRegistry registre) { return new CountedAspect(registre); }@Timed(value = "ciclourbana.tarifa.calcul",
description = "Temps de calcul de l'import",
extraTags = {"component", "tarifes"},
percentiles = {0.5, 0.95, 0.99})
public BigDecimal calcular(TipusTarifa tipus, Duration durada) { ... }
@Counted(value = "ciclourbana.cobraments.intents", recordFailuresOnly = false)
public ResultatCobrament cobrar(Long idLloguer, BigDecimal importTotal) { ... }@Timed genera un Timer amb etiquetes class, method i exception —aquesta última val none quan no hi ha hagut error, cosa que permet separar la latència dels èxits de la de les fallades—. @Counted compta invocacions amb les mateixes etiquetes.
Els seus límits, que convé tenir clars abans de fer-les servir pertot arreu. Pateixen el parany del proxy per quarta vegada al curs: una crida interna no es mesura. No poden etiquetar per dades del negoci: extraTags només admet constants, així que no hi ha manera d'etiquetar per tipusTarifa rebut com a argument —per a això cal el codi explícit de l'apartat 7—. I afegeixen un aspecte per mètode anotat, amb el seu cost d'invocació.
El criteri de CicloUrbana: @Timed per a instrumentació transversal i ràpida —temporitzar un servei sencer mentre s'investiga alguna cosa—, i codi explícit per a les mètriques de negoci que porten etiquetes i que alimentaran quadres de comandament permanents.
- Percentils davant d'histogrames
Aquest apartat és el més tècnic de la lliçó i el que més conseqüències té en escalar. A 09-01 va quedar clar que el SLO s'escriu en p95 i p99. La pregunta ara és on es calculen aquests percentils, i hi ha dues respostes incompatibles.
management.metrics.distribution:
percentiles:
ciclourbana.tarifa.calcul: 0.5, 0.95, 0.99 # opcio A: al client
percentiles-histogram:
http.server.requests: true # opcio B: histograma
ciclourbana.lloguer.durada: true
slo:
http.server.requests: 100ms, 300ms, 500ms, 1s, 3s
minimum-expected-value:
http.server.requests: 10ms
maximum-expected-value:
http.server.requests: 10spercentiles (calculats al client) |
percentiles-histogram (cubetes) |
|
|---|---|---|
| Què exporta | Tres números ja calculats: quantile="0.95" |
Moltes sèries _bucket{le="..."} |
| On es calcula | A la JVM, amb un estimador aproximat | Al sistema de monitoratge, amb histogram_quantile |
| Agregable entre instàncies | No | Sí |
| Sèries generades | Poques | Desenes per mètrica |
| Configurable a posteriori | No: cal tornar a desplegar | Sí: qualsevol percentil, sense tocar l'aplicació |
| Quan fer-lo servir | Una instància, o una ullada local | Producció amb diverses instàncies |
Per què els percentils del client no es poden agregar. És la conseqüència d'aquella regla de 09-01: els percentils no es promitgen. Si la instància 1 informa d'un p95 = 200 ms i la instància 2 d'un p95 = 400 ms, el p95 real del servei no és 300 ms, i no hi ha manera de calcular-lo a partir d'aquests dos números: la informació necessària es va perdre en calcular-los. Amb tres rèpliques a Kubernetes (08-04), aquest gràfic de «p95» és senzillament fals.
Com ho resol l'histograma. En lloc d'un percentil, cada instància exporta quantes observacions van caure per sota de cada llindar: 1.200 per sota de 100 ms, 1.380 per sota de 300 ms, i així. Aquests recomptes sí que se sumen entre instàncies, i a partir de la suma s'interpola qualsevol percentil amb histogram_quantile (09-04). Es decideix el percentil en consultar, no en instrumentar.
El preu és la cardinalitat: un histograma genera una sèrie per cubeta. Per això existeixen els tres ajustos de dalt: slo afegeix cubetes exactament als llindars que t'importen —molt útil, perquè permet després calcular directament «quin percentatge de peticions va baixar de 300 ms», que és la formulació literal d'un objectiu de servei—, i minimum-expected-value / maximum-expected-value acoten el rang, eliminant les cubetes absurdes per sota de 10 ms o per sobre de 10 s i reduint molt el nombre de sèries.
La decisió de CicloUrbana: histogrames en les poques mètriques que sostenen els SLO (http.server.requests i la durada de lloguers) i res de distribució a la resta. Activar percentiles-histogram per a tot és una de les maneres més ràpides de provocar el problema de l'apartat 6.
MeterFilter i MeterRegistryCustomizer
MeterFilter i MeterRegistryCustomizerQuan cal corregir la instrumentació —pròpia o d'una biblioteca que no controles—, MeterFilter intervé en el registre de cada mesurador:
@Bean
MeterRegistryCustomizer<MeterRegistry> personalitzarRegistre() {
return registre -> registre.config()
// 1. Denegar metriques cares que ningu no fa servir
.meterFilter(MeterFilter.denyNameStartsWith("jvm.buffer"))
// 2. Limitar la cardinalitat d'una etiqueta: xarxa de seguretat de l'apartat 6
.meterFilter(MeterFilter.maximumAllowableTags(
"ciclourbana.lloguers.iniciats", "estacio", 100,
MeterFilter.deny()))
// 3. Sostre global de series per metrica
.meterFilter(MeterFilter.maximumAllowableMetrics(5_000))
// 4. Reanomenar una metrica heretada sense tocar el codi que la publica
.meterFilter(MeterFilter.renameTag("ciclourbana.lloguers.iniciats",
"tipus", "tarifa"))
// 5. Regles de distribucio per codi
.meterFilter(new MeterFilter() {
@Override
public DistributionStatisticConfig configure(Meter.Id id,
DistributionStatisticConfig config) {
return id.getName().startsWith("ciclourbana.")
? DistributionStatisticConfig.builder()
.percentilesHistogram(true).build().merge(config)
: config;
}
});
}Els cinc usos, en ordre d'utilitat real. Denegar mètriques que no es fan servir redueix la mida de cada recollida —jvm.buffer i algunes de tomcat rarament es miren—. maximumAllowableTags és la xarxa de seguretat contra l'explosió de cardinalitat: passades 100 estacions diferents, deixa de registrar sèries noves en lloc de tombar Prometheus; és un salvavides, no una excusa per etiquetar malament. maximumAllowableMetrics posa un sostre global. renameTag i MeterFilter.commonTags permeten adaptar mètriques de tercers a la teva convenció. I la configuració de distribució per codi aplica regles a famílies senceres de mètriques per prefix, cosa que el YAML no permet.
Convé distingir els dos beans: MeterRegistryCustomizer s'executa una vegada sobre el registre, mentre que el MeterFilter que hi instal·la s'executa per a cada mesurador que es registri. I hi ha un ordre que sorprèn: els filtres s'apliquen en l'ordre en què s'afegeixen, i un deny posterior no revoca un mesurador ja registrat, així que els filtres s'han d'instal·lar abans que l'aplicació comenci a instrumentar —d'aquí que es facin en un @Bean i no en un @PostConstruct qualsevol—.
- El pool, la base de dades i la memòria cau: quins números vigilar
Amb el que hem après a 09-01 i 09-02, aquestes són les mètriques que tradueixen aquells diagnòstics puntuals en vigilància contínua:
| Mètrica | Valor sa | Què significa si es degrada |
|---|---|---|
hikaricp.connections.pending |
0 gairebé sempre | Fils esperant connexió: consultes lentes o pool curt |
hikaricp.connections.acquire (p99) |
< 10 ms | El mateix, amb magnitud |
hikaricp.connections.usage (p99) |
< durada de la transacció esperada | Connexió retinguda: crida remota dins de la transacció |
hikaricp.connections.timeout |
0 | S'està esgotant el connection-timeout: incident en curs |
spring.data.repository.invocations (_count per mètode) |
Estable per petició | Si creix amb el volum de dades: N+1 |
cache.gets{result="hit"} / total |
> 0,9 | Memòria cau inútil, clau mal triada o autoinvocació (09-02) |
cache.evictions |
A prop de 0 | maximumSize massa petita |
jvm.gc.pause (_max) |
< 200 ms amb G1 | Pauses que expliquen el p99 |
jvm.memory.used{area="heap"} després de GC |
Baixa després de cada cicle | Si només puja: fuita |
tomcat.threads.busy / tomcat.threads.config.max |
< 0,8 | Saturació del contenidor web |
La més rendible de totes és la penúltima combinada amb jvm.gc.pause: memòria que puja i no baixa després de les pauses és la signatura d'una fuita, i veure-la en un gràfic d'un mes és infinitament més fàcil que en un bolcat de heap. I la primera, pending, és la que converteix el diagnòstic manual de l'exercici 1 de 09-01 en una alerta automàtica.
- L'Observation API: una instrumentació, dues senyals
Fins aquí hem registrat mètriques. Quan a 09-06 calgui registrar traces, apareixeria el problema d'instrumentar dues vegades el mateix. Spring Boot 3 ho resol amb la Micrometer Observation API: es declara una observació i la infraestructura produeix alhora una mètrica, un span de traça i, si es configura, una entrada de log.
@Service
public class CalculadoraTarifaService {
private final ObservationRegistry observacions;
public BigDecimal calcular(TipusTarifa tipus, Duration durada) {
return Observation.createNotStarted("ciclourbana.tarifa.calcul", observacions)
.contextualName("calcul-tarifa")
.lowCardinalityKeyValue("tarifa", tipus.name()) // -> etiqueta de metrica
.highCardinalityKeyValue("minuts", String.valueOf(durada.toMinutes()))
.observe(() -> calculadora.importTotal(tipus, durada)); // -> nomes a la traca
}
}La distinció entre lowCardinalityKeyValue i highCardinalityKeyValue és la regla de l'apartat 6 convertida en API, i és una de les millors idees de Micrometer: les claus de baixa cardinalitat es converteixen en etiquetes de la mètrica i en atributs del span; les d'alta cardinalitat van només al span, on no fan mal. El model obliga a pensar en la cardinalitat en escriure el codi, que és justament on cal pensar-hi.
La versió declarativa necessita l'aspecte corresponent:
@Bean ObservedAspect observedAspect(ObservationRegistry registre) {
return new ObservedAspect(registre);
}
@Observed(name = "ciclourbana.tarifa.calcul", contextualName = "calcul-tarifa")
public BigDecimal calcular(TipusTarifa tipus, Duration durada) { ... }Avui, amb només Micrometer Metrics al classpath, això produeix un Timer idèntic al de @Timed. A 09-06, en afegir Micrometer Tracing, el mateix codi començarà a produir spans sense tocar una línia. Aquesta és la raó per conèixer-lo ja: el que s'instrumenti amb l'Observation API estarà llest per a la traçabilitat distribuïda; el que s'instrumenti amb Timer a mà, no.
Com a complement, un ObservationHandler propi permet reaccionar a cada observació —per exemple, escriure una línia de log amb la seva durada— i ObservationPredicate permet filtrar quines es registren, típicament per ignorar /actuator/** i no mesurar-se a un mateix.
- De les mètriques als SLO: RED i USE
Instrumentar sense un mètode produeix centenars de gràfics que ningú no mira. Els dos mètodes de referència diuen què cal mirar i són complementaris:
RED, per a serveis (el que veu l'usuari): Rate (peticions per segon), Errors (proporció que falla) i Duration (distribució de la latència).
USE, per a recursos (el que consumeix el sistema): Utilization (percentatge d'ús), Saturation (feina encuada esperant) i Errors (fallades del recurs).
| Mètode | Aplicat a | Senyal | Mètrica de CicloUrbana | SLO proposat |
|---|---|---|---|---|
| RED | API de lloguers | Rate | http.server.requests _count sobre /api/v1/lloguers |
Informatiu |
| RED | API de lloguers | Errors | Proporció amb outcome="SERVER_ERROR" |
< 0,5 % en 5 min |
| RED | API de lloguers | Duration | http.server.requests p99 |
< 800 ms |
| RED | Passarel·la de pagaments | Errors | resilience4j.circuitbreaker.state |
Circuit tancat > 99 % |
| USE | Pool de connexions | Utilization | hikaricp.connections.active / max |
< 0,8 |
| USE | Pool de connexions | Saturation | hikaricp.connections.pending |
= 0 |
| USE | Fils de Tomcat | Utilization | tomcat.threads.busy / max |
< 0,8 |
| USE | JVM | Saturation | jvm.gc.pause _max |
< 200 ms |
| Negoci | Servei de Ribalta | — | ciclourbana.lloguers.iniciats |
> 0 en hora punta |
L'última fila és la més valuosa i la que gairebé ningú no posa. Tots els indicadors tècnics poden estar verds mentre el servei està trencat: un desplegament que va trencar el botó de llogar a l'app deixa l'API contestant 200 a les consultes d'estacions i zero lloguers iniciats. Una alerta sobre aquesta mètrica de negoci detecta en cinc minuts el que les tècniques no detecten mai.
La implementació d'aquestes alertes és feina de 09-04, amb regles a Prometheus i Alertmanager. El que es decideix aquí és què es mesura i quin llindar es considera acceptable, i aquesta decisió és de producte tant com d'enginyeria.
- Provar que una mètrica es registra
Una mètrica de negoci és comportament observable: si una refactorització deixa d'incrementar el comptador de lloguers, el quadre de comandament menteix i ningú no se n'assabenta. Es prova amb SimpleMeterRegistry, la implementació en memòria de Micrometer.
class MetriquesCicloUrbanaTest {
private final MeterRegistry registre = new SimpleMeterRegistry();
private final MetriquesCicloUrbana metriques = new MetriquesCicloUrbana(registre);
@Test
void comptaElsLloguersPerTipusDeTarifa() {
metriques.lloguerIniciat(TipusTarifa.ESTANDARD, 1L);
metriques.lloguerIniciat(TipusTarifa.ESTANDARD, 1L);
metriques.lloguerIniciat(TipusTarifa.ESTUDIANT, 2L);
assertThat(registre.get("ciclourbana.lloguers.iniciats")
.tag("tarifa", "ESTANDARD").counter().count()).isEqualTo(2.0);
assertThat(registre.get("ciclourbana.lloguers.iniciats")
.tag("tarifa", "ESTUDIANT").counter().count()).isEqualTo(1.0);
}
@Test
void elGaugeSobreviuAlRecolectorDeBrossa() {
metriques.actualitzarDisponibles(1L, "Plaça Major", 12);
System.gc(); // forca l'escenari de l'apartat 8
assertThat(registre.get("ciclourbana.bicicletes.disponibles")
.tag("estacio", "Plaça Major").gauge().value()).isEqualTo(12.0);
}
}La segona prova és la joia: verifica que la referència forta existeix, que és precisament l'error que es manifesta només en producció i després d'hores d'execució. System.gc() és un suggeriment i no una garantia, però a la pràctica n'hi ha prou perquè la prova falli amb la versió incorrecta de l'apartat 8.
En integració, @SpringBootTest amb @AutoConfigureObservability aixeca el registre real i permet comprovar que les mètriques HTTP automàtiques apareixen —per defecte, les proves desactiven l'exportació—:
@SpringBootTest
@AutoConfigureObservability
class MetriquesHttpIT {
@Autowired MeterRegistry registre;
@Autowired TestRestTemplate client;
@Test
void laPeticioQuedaRegistradaAmbLaSevaUriPlantilla() {
client.getForEntity("/api/v1/estacions/1", EstacioResponse.class);
assertThat(registre.get("http.server.requests")
.tag("uri", "/api/v1/estacions/{id}") // plantilla, no l'1
.timer().count()).isEqualTo(1L);
}
}Errors Comuns i Consells
Etiquetar per identificador d'usuari, per URI amb identificadors o per identificador de traça. És l'error greu de la lliçó: explosió de cardinalitat que tomba primer Prometheus i després l'aplicació, setmanes després d'haver-se introduït. El que identifica un individu va al log o a la traça.
Perdre el Gauge per la referència feble. Funciona una estona i després retorna NaN. Desa sempre una referència forta en un camp del component, i escriu la prova amb System.gc().
Fer servir un Counter per a alguna cosa que baixa. «Lloguers en curs» no és un comptador: és un Gauge. Un comptador que es decrementa trenca rate() i produeix gràfics absurds.
Creure que percentiles serveix amb diverses instàncies. Els percentils calculats a la JVM no es poden agregar; amb tres rèpliques, aquest gràfic és fals. Es fan servir histogrames.
Activar percentiles-histogram per a totes les mètriques. Multiplica les sèries per deu. Només en les que sostenen un SLO, i acotades amb minimum/maximum-expected-value.
Fer feina dins de la funció d'un Gauge. S'invoca a cada recollida, cada 15 segons, per sempre. Una consulta a la base de dades allà és una càrrega permanent que ningú no relaciona amb res.
Mesurar només el tècnic. Amb tots els indicadors en verd el servei pot estar trencat. La mètrica que avisa d'un desplegament que va trencar el lloguer és ciclourbana.lloguers.iniciats caient a zero.
Consell: centralitza els noms de mètriques en un component o en constants. Els noms són un contracte amb els quadres de comandament i les alertes de 09-04; un canvi de nom trenca panells en silenci. I segueix la convenció de Micrometer —minúscules amb punts, prefix d'aplicació—, deixant que cada exportador la tradueixi.
Consell: fes servir l'Observation API per al que sigui nou. Costa el mateix que un Timer i a 09-06 produirà spans sense tocar el codi.
Consell: posa les etiquetes comunes (application, entorn, instancia) des del primer dia. Afegir-les després obliga a reescriure totes les consultes i a perdre la continuïtat dels gràfics.
Exercicis
Exercici 1: triar el mesurador i les etiquetes
Per a cada necessitat de l'ajuntament de Ribalta, indica el tipus de mesurador, el nom, les etiquetes —justificant-ne la cardinalitat— i si necessita histograma: (a) quantes bicicletes hi ha ara mateix en manteniment, per taller (hi ha 2 tallers); (b) quants cobraments ha rebutjat la passarel·la, distingint el motiu (SALDO, TARGETA_CADUCADA, TECNIC); (c) quant triga la importació nocturna de l'ajuntament, sabent que dura entre 20 i 50 minuts i que es vol vigilar mentre s'executa; (d) l'import de cada lloguer facturat, per poder respondre «quin és l'import mitjà i el p95?»; (e) quantes vegades cada ciutadà ha llogat aquest mes.
Exercici 2: diagnosticar una instrumentació trencada
Un company ha afegit aquesta instrumentació. Tres setmanes després, Prometheus consumeix 12 GB de memòria, /actuator/prometheus triga 8 segons a respondre i el gràfic de bicicletes disponibles mostra buits. Troba els tres problemes, explica el símptoma de cadascun i escriu la versió corregida.
@RestController
public class LloguerController {
@PostMapping("/api/v1/lloguers")
public ResponseEntity<LloguerResponse> iniciar(@RequestBody IniciarLloguerRequest p,
Authentication auth) {
LloguerResponse r = lloguerService.iniciar(p);
Counter.builder("lloguers")
.tag("usuari", auth.getName())
.tag("uri", "/api/v1/lloguers/" + r.id())
.register(registre).increment();
Gauge.builder("bicis.lliures", new AtomicInteger(comptarLliures()), AtomicInteger::get)
.register(registre);
return ResponseEntity.status(201).body(r);
}
}Exercici 3: definir els SLO de CicloUrbana
L'ajuntament demana un acord de nivell de servei. Defineix els SLI i SLO de CicloUrbana seguint RED i USE: tria quatre indicadors de servei i tres de recursos, digues amb quina mètrica de Micrometer es calcula cadascun, proposa un llindar i un període d'avaluació, i indica quins d'aquests llindars haurien de despertar algú de matinada i quins no. Justifica la configuració de management.metrics.distribution que fa possible mesurar-los.
Solucions
Solució 1.
(a) Bicicletes en manteniment per taller: Gauge, ciclourbana.bicicletes.manteniment, etiqueta taller (2 valors: cardinalitat mínima). És un valor que puja i baixa, per tant no és un comptador. Sense histograma —els gauges no tenen distribució—. Compte amb la referència forta de l'apartat 8: un Map<String, AtomicInteger> com a camp, refrescat des de la tasca programada, i mai una consulta a la base de dades dins de la funció del gauge.
(b) Cobraments rebutjats: Counter, ciclourbana.cobraments.rebutjats, etiqueta motiu amb 3 valors fixos de l'enumerat. Només puja i el que interessa és el seu ritme, que rate() extraurà a 09-04. L'etiqueta ha de sortir d'un enumerat, mai del missatge d'error de la passarel·la: un text lliure retornat per un tercer és cardinalitat il·limitada disfressada.
(c) Importació nocturna: LongTaskTimer, ciclourbana.importacio.durada. És el cas que justifica la seva existència: un Timer normal no publicaria res durant els 40 minuts que dura i només registraria el resultat en acabar, amb la qual cosa seria impossible saber si està en marxa o penjada. El LongTaskTimer publica active_tasks i duration de les tasques en curs, i permet alertar de «fa més de 60 minuts que dura». Complement útil: un Gauge amb la marca de temps de l'última execució correcta, per detectar que no s'ha executat.
(d) Import facturat: DistributionSummary, ciclourbana.lloguer.import, baseUnit("euros"), amb publishPercentileHistogram() perquè la pregunta demana explícitament mediana i p95 i hi ha diverses instàncies. No és un Timer perquè no mesura temps. Etiqueta raonable: tarifa (3 valors). Convé acotar amb minimum-expected-value: 0.5 i maximum-expected-value: 50 per no generar cubetes inútils.
(e) Lloguers per ciutadà i mes: cap mètrica. És cardinalitat il·limitada —una sèrie per ciutadà— i a més no és una pregunta de monitoratge sinó de negoci, amb la fila del lloguer ja desada a PostgreSQL: es respon amb SELECT usuari_id, count(*) ... GROUP BY usuari_id. La regla de l'apartat 6 en la seva forma més pura: les mètriques responen «quants» agregat, no «qui».
Solució 2.
Problema 1 — etiqueta usuari de cardinalitat il·limitada. Una sèrie per ciutadà; amb 50.000 usuaris, 50.000 sèries d'un sol comptador. Símptoma: memòria de Prometheus disparada i consultes lentes. És la causa principal dels 12 GB.
Problema 2 — etiqueta uri amb l'identificador del lloguer. Cardinalitat infinita: una sèrie nova per cada lloguer creat, per sempre, i cap d'elles no es reutilitza mai. Símptoma: /actuator/prometheus retornant megabytes i trigant 8 segons, perquè la seva mida creix linealment amb el nombre de lloguers històrics. És fins i tot pitjor que l'anterior, perquè no té sostre.
Problema 3 — Gauge registrat a cada petició sobre un objecte temporal. Dues fallades en una línia: es registra dins del mètode, així que s'intenta crear un mesurador per petició, i l'AtomicInteger no té referència forta, així que el GC l'allibera i el gauge retorna NaN. Símptoma: els buits al gràfic. A més, comptarLliures() al camí de la petició afegeix una consulta a cada lloguer.
Versió corregida:
@RestController
public class LloguerController {
private final MetriquesCicloUrbana metriques; // el component de l'apartat 7
@PostMapping("/api/v1/lloguers")
public ResponseEntity<LloguerResponse> iniciar(@RequestBody IniciarLloguerRequest p) {
LloguerResponse r = lloguerService.iniciar(p); // el servei ja instrumenta
return ResponseEntity.status(201).body(r);
}
}Les decisions: la instrumentació se'n va al servei, amb metriques.lloguerIniciat(tipusTarifa, idEstacio) i les seves dues etiquetes de baixa cardinalitat; la URI i l'estat ja els aporta http.server.requests amb la plantilla, de manera que el comptador manual de peticions sobrava completament; i el gauge de bicicletes lliures es registra una vegada a MetriquesCicloUrbana, amb la seva referència forta i actualitzat des de la tasca programada de 07-03. Com a xarxa de seguretat, s'hi afegeix el MeterFilter.maximumAllowableTags(...) de l'apartat 11, que hauria convertit aquesta caiguda en una mètrica truncada.
Solució 3.
Indicadors de servei (RED), mesurats sobre http.server.requests:
| SLI | Càlcul | SLO | Període | Desperta? |
|---|---|---|---|---|
| Disponibilitat de l'API | 1 − (outcome="SERVER_ERROR" / total) |
≥ 99,5 % | 30 dies | Sí si > 5 % en 5 min |
| Latència d'inici de lloguer | p99 d'uri="/api/v1/lloguers" |
< 800 ms | 5 min | Sí, sostingut 15 min |
| Latència de consulta d'estacions | p95 d'uri="/api/v1/estacions" |
< 300 ms | 5 min | No: avís en horari laboral |
| Lloguers iniciats en hora punta | rate de ciclourbana.lloguers.iniciats |
> 0 entre les 7 i les 10 h | 10 min | Sí: el servei està trencat |
Indicadors de recursos (USE):
| SLI | Mètrica | Llindar | Desperta? |
|---|---|---|---|
| Saturació del pool | hikaricp.connections.pending |
0; alerta si > 5 durant 5 min | Sí: precedeix una caiguda |
| Utilització de fils de Tomcat | tomcat.threads.busy / config.max |
< 0,8 | No: avís |
| Pauses de GC | jvm.gc.pause _max |
< 200 ms | No: avís, llevat que la latència es degradi |
Què desperta i què no. El criteri és doble: l'usuari ho està patint ara i hi ha alguna cosa a fer. Per això desperten la taxa d'error, la latència de l'endpoint crític sostinguda, l'absència de lloguers en hora punta i el pool saturat —que és l'avís primerenc d'una caiguda completa—. No desperten les pauses de GC ni la utilització de fils: són causes, no símptomes, i el seu lloc és un avís en horari laboral. És el principi que es desenvoluparà a 09-04: alertar sobre símptomes, no sobre causes, perquè cada alerta que no exigeix acció immediata erosiona la credibilitat de totes les altres.
Configuració necessària:
management.metrics.distribution:
percentiles-histogram:
http.server.requests: true # agregable entre les 3 instancies
slo:
http.server.requests: 300ms, 800ms # cubetes just als llindars dels SLO
minimum-expected-value.http.server.requests: 10ms
maximum-expected-value.http.server.requests: 5sHistograma i no percentiles, perquè amb tres rèpliques els percentils calculats a cada JVM no es poden agregar i donarien una xifra falsa (apartat 10). Les cubetes de slo a 300 ms i 800 ms permeten respondre directament «quin percentatge de peticions va complir l'objectiu», que és la formulació literal del SLO i la que es portarà a l'informe mensual de l'ajuntament. I l'acotament entre 10 ms i 5 s evita desenes de cubetes inútils, mantenint la cardinalitat sota control.
Conclusió
CicloUrbana ja no només es deixa preguntar: es mesura sola i contínuament. Tens clars els tres pilars de l'observabilitat i què respon cadascun —mètriques per detectar, traces per localitzar, logs per entendre—, i entens Micrometer com la façana que fa que el teu codi registri comptadors sense saber qui els recollirà, amb MeterRegistry injectable com un bean més. Saps triar entre els sis tipus de mesurador amb criteris que resolen els dubtes reals: comptador per a «quantes vegades» i gauge per a «quant n'hi ha ara», Timer per a operacions del programa i DistributionSummary per a magnituds de negoci, LongTaskTimer per al que cal vigilar mentre passa.
Coneixes el catàleg de mètriques que existeixen sense escriure codi —amb http.server.requests com la més valuosa del sistema— i saps consultar-les per /actuator/metrics amb ?tag=, amb la limitació que allà no hi ha història. I tens gravada la regla d'or d'aquesta lliçó: la cardinalitat d'una etiqueta ha de ser baixa i acotada; tarifa i estacio sí, idUsuari, la URI amb identificadors i el rastreId mai, perquè el que identifica un individu pertany a un log o a una traça i no a una mètrica.
Has instrumentat la xarxa de Ribalta amb MetriquesCicloUrbana: comptadors de lloguers iniciats i finalitzats per tarifa, un gauge de bicicletes disponibles per estació —amb la referència forta que evita el NaN de la referència feble, i la seva prova amb System.gc()—, un Timer del càlcul de tarifa i un DistributionSummary de la durada dels trajectes, tot darrere d'una façana que deixa el domini sense conèixer Micrometer. Saps quan @Timed i @Counted són suficients i on són els seus tres límits. Entens la diferència que més importa en escalar: els percentils calculats a la JVM no s'agreguen entre instàncies i els histogrames sí, amb slo, minimum-expected-value i maximum-expected-value perquè aquesta potència no es pagui amb una explosió de sèries. I saps corregir i protegir la instrumentació amb MeterFilter, inclosa la xarxa de seguretat de maximumAllowableTags.
Finalment, tens els números que cal vigilar al pool, la base de dades i les memòries cau de 09-02; l'Observation API com el model unificat que converteix la cardinalitat en API (lowCardinalityKeyValue davant de highCardinalityKeyValue) i que a 09-06 produirà spans sense tocar el codi; i els mètodes RED i USE convertits en SLI i SLO concrets, inclosa la fila que gairebé ningú no posa i que detecta un servei trencat amb tot en verd: lloguers iniciats = 0 en hora punta.
Queda el problema que ha estat present tota l'estona: tot això viu en memòria i mor amb el procés. Cada desplegament de 08-05 esborra les mètriques; /actuator/metrics no té història; no hi ha gràfics, no hi ha comparació amb la setmana passada i, sobretot, no hi ha ni una sola alerta. Els SLO de l'apartat 14 estan definits però ningú no els vigila. La lliçó següent, Ús de Prometheus i Grafana, tanca aquest buit: el model de recollida per consulta, el format de /actuator/prometheus, PromQL de debò —rate, sum by, histogram_quantile sobre les cubetes que acabem de configurar—, el quadre de comandament de CicloUrbana panell a panell i les regles d'alerta que converteixen aquests números en una trucada de telèfon quan de debò cal.
Curs de Spring Boot
Mòdul 1: Introducció a Spring Boot
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
