A 06-04 vas muntar el primer pilar de l'observabilitat. AlpinaShop té ara un tauler, comprovacions de disponibilitat des de quatre continents i tres alertes que arriben al mòbil de la Marta. És un salt enorme respecte a esperar el correu d'un client.
Però fixa't en què passa quan aquesta alerta salta de veritat. Són les onze de la nit i el mòbil de la Marta mostra: «Taxa d'error 5xx per sobre del 2 % durant 5 minuts». La Marta obre el tauler. Ho confirma: el 4,3 % de les peticions fallen, la latència p95 ha pujat de 400 ms a 3,2 segons, i va començar fa uns vint minuts. I ara, què?
La mètrica li ha dit que alguna cosa va malament. No li diu què. No li diu quina excepció s'està llançant, ni a quins clients els passa, ni si és al catàleg o al carret, ni si el culpable és l'aplicació, la base de dades o una crida a una API externa. Per a això calen els altres dos pilars.
Aquesta lliçó els construeix. I acaba amb el recorregut complet d'aquest incident, des de l'alerta fins a la línia de codi exacta, perquè aquesta correlació —saltar de l'alerta a la mètrica, de la mètrica al registre i del registre a la traça— és la raó per la qual existeix l'observabilitat.
Contingut
- L'entrada de registre estructurada
- D'on surten els registres automàticament
- Emetre registres bé des de l'aplicació Flask
- L'explorador de registres i el seu llenguatge de consultes
- Consultes que resolen incidents reals d'AlpinaShop
- Buckets, retenció, vistes i àmbits
- Sortidors: exportar a BigQuery, Cloud Storage i Pub/Sub
- Filtres d'exclusió: no pagar per soroll
- Mètriques basades en registres
- Cloud Trace: què és el rastreig distribuït
- Instrumentar Flask amb OpenTelemetry
- Llegir una cascada i trobar la consulta lenta
- Mostreig i cost de les traces
- Cloud Profiler: perfilatge continu
- La correlació que ho uneix tot: un incident de principi a fi
- Cost per volum i les tres decisions que el controlen
- L'entrada de registre estructurada
Un registre no és una línia de text. A Cloud Logging, una entrada de registre és un objecte amb camps, i entendre aquests camps és el que converteix els registres d'un abocador en una base de dades consultable.
| Camp | Què conté | Per què importa |
|---|---|---|
timestamp |
Quan va passar | Ordenar i correlacionar |
resource |
Quin recurs el va emetre, amb les seves etiquetes | Filtrar per servei, clúster, instància |
severity |
DEBUG, INFO, WARNING, ERROR, CRITICAL |
Filtrar per gravetat |
logName |
Nom del registre | Separar accessos, aplicació, auditoria |
textPayload |
Text pla | El que emet un print |
jsonPayload |
Objecte JSON amb els teus camps | El que permet consultar de veritat |
labels |
Etiquetes pròpies clau-valor | Dimensions de negoci |
httpRequest |
Mètode, URL, codi, latència, IP | Analítica d'accés |
trace |
Identificador de la traça | La clau de la correlació (apartat 15) |
spanId |
Identificador del tram dins de la traça | Precisió en la correlació |
insertId |
Identificador únic de l'entrada | Deduplicació |
operation |
Agrupa entrades d'una operació llarga | Seguir un procés per passos |
La diferència entre textPayload i jsonPayload és la diferència entre poder investigar i no poder.
Sense estructura:
És llegible per a un humà i opac per a una màquina. Per respondre a «quantes comandes van fallar per timeout de la passarel·la l'última hora, i de quin import?» cal analitzar text amb expressions regulars, i n'hi ha prou que algú canviï el missatge perquè tot es trenqui.
Amb estructura:
{
"severity": "ERROR",
"jsonPayload": {
"mensaje": "Error processant comanda",
"id_pedido": "8842",
"id_cliente_hash": "a3f9c1...",
"causa": "timeout_pasarela",
"importe_eur": 249.90,
"duracion_ms": 5012,
"reintento": 2
},
"labels": {"componente": "checkout", "version": "a3f9c1b"},
"trace": "projects/alpinashop-prod/traces/4bf92f3577b34da6a3ce929d0e0e4736"
}Ara aquesta pregunta és un filtre. I la penúltima línia permet una cosa encara més potent: saltar directament a la traça completa d'aquesta petició, amb tot el que va passar abans i després. Hi tornarem.
- D'on surten els registres automàticament
Igual que amb les mètriques, hi ha una bona notícia: bona part dels registres ja s'estan recollint sense que hagis fet res.
| Origen | Registre | Què conté | Activat per defecte? |
|---|---|---|---|
| Balancejador | requests |
Cada petició: URL, codi, latència, IP, país | No: cal activar-ho |
| Cloud SQL | postgres.log / mysql-error.log |
Errors, consultes lentes | Parcialment |
| GKE | stdout / stderr dels contenidors |
El que l'aplicació escriu | Sí |
| GKE | events |
Esdeveniments de Kubernetes | Sí |
| Cloud Run / Functions | stdout / stderr + peticions |
Aplicació i accessos | Sí |
| VPC Flow Logs | vpc_flows |
Connexions de xarxa, bytes | No: cal activar-ho |
| Cloud Armor | Dins del registre del balancejador | Regles aplicades i bloquejos | Amb el registre del LB |
| Registres d'auditoria | activity |
Qui va fer què a l'API | Sí (activitat d'administració) |
| Registres d'auditoria | data_access |
Qui va llegir quines dades | No: s'activa i costa |
Dues caselles mereixen atenció.
Els registres del balancejador no estan activats per defecte, i són dels més valuosos que existeixen: contenen cada petició a la botiga amb la seva latència, el seu codi, el seu país d'origen i el resultat de la memòria cau de CDN. Activar-los:
gcloud compute backend-services update bs-catalogo-web \
--global \
--enable-logging \
--logging-sample-rate=1.0 \
--project=alpinashop-prodEl --logging-sample-rate=1.0 registra el 100 % de les peticions. És el correcte mentre el volum sigui moderat; amb milions de peticions diàries es baixa a 0,1 o 0,05 i s'accepta perdre detall a canvi de cost. Comença per 1.0 i ajusta quan vegis la factura.
Els VPC Flow Logs registren les connexions de xarxa i són imprescindibles per diagnosticar problemes de connectivitat i per investigar incidents de seguretat. El seu volum és alt, així que s'activen amb mostreig:
gcloud compute networks subnets update sn-web-euw1 \
--region=europe-west1 \
--enable-flow-logs \
--logging-aggregation-interval=interval-5-sec \
--logging-flow-sampling=0.5 \
--project=alpinashop-prodI els registres d'auditoria mereixen una menció amb remissió: els d'activitat d'administració —qui va crear, modificar o esborrar un recurs— estan sempre actius i són gratuïts. Els d'accés a dades —qui va llegir què— cal activar-los, generen un volum enorme i es paguen. La política d'auditoria a nivell d'organització, amb les seves implicacions de compliment, és tema de 07-07; aquí n'hi ha prou de saber que existeixen i que responen a la pregunta «qui va esborrar aquest recurs?».
- Emetre registres bé des de l'aplicació Flask
Aquí és on l'equip d'AlpinaShop té més marge de millora. El catàleg avui fa això:
Funciona en el sentit que el text acaba a Cloud Logging —a GKE, tot el que va a stdout es recull—. Però produeix entrades amb textPayload, totes amb severitat INFO encara que siguin errors, sense cap camp consultable i sense identificador de traça.
La forma correcta, amb la biblioteca client:
# catalogo/registro.py
import logging
import google.cloud.logging
from google.cloud.logging.handlers import StructuredLogHandler
from google.cloud.logging_v2.handlers import setup_logging
# A GKE i Cloud Run, StructuredLogHandler escriu JSON a stdout
# i l'agent el recull. Sense crides extra a l'API: és el més eficient.
handler = StructuredLogHandler()
setup_logging(handler)
log = logging.getLogger("catalogo")
log.setLevel(logging.INFO)I el seu ús, amb els camps com a dades i no com a text:
# catalogo/pedidos.py
from catalogo.registro import log
def procesar_pedido(pedido):
# El diccionari 'json_fields' es converteix en jsonPayload
log.info("Comanda rebuda", extra={"json_fields": {
"id_pedido": pedido.id,
"importe_eur": float(pedido.importe),
"num_lineas": len(pedido.lineas),
"canal": pedido.canal,
}})
try:
resultado = cobrar(pedido)
except TimeoutPasarela as e:
# log.exception afegeix automàticament el traceback complet
log.exception("Timeout a la passarel·la de pagament", extra={"json_fields": {
"id_pedido": pedido.id,
"importe_eur": float(pedido.importe),
"causa": "timeout_pasarela",
"duracion_ms": e.duracion_ms,
}})
raiseLes cinc regles d'un registre útil, que valen més que qualsevol biblioteca:
| Regla | Malament | Bé |
|---|---|---|
| Les dades van en camps, no al missatge | f"comanda {id} va fallar" |
"Comanda fallida" + {"id_pedido": id} |
| La severitat ha de ser correcta | Tot INFO |
ERROR per a errors, WARNING per a avisos |
| Mai dades personals | Correu, nom, targeta | Hash de l'identificador de client |
| Context suficient per actuar | "Error" |
Quina operació, sobre què, per què va fallar |
| Sense soroll | Un registre per cada iteració d'un bucle | Un registre per operació, amb el resum |
La tercera regla no és negociable i va més enllà del bon gust. Els registres es retenen, s'exporten a BigQuery, els llegeixen diverses persones i sobreviuen mesos. Un correu electrònic o un número de targeta escrit en un registre és una fuita de dades personals amb implicacions de RGPD, coherent amb tot el que s'ha vist a 04-07 sobre DLP. Si necessites identificar un client als registres, fes servir un hash estable que permeti correlacionar sense identificar.
Sobre la severitat, un criteri pràctic que evita discussions eternes:
| Nivell | Quan | Genera alerta? |
|---|---|---|
DEBUG |
Detall de desenvolupament | Mai en producció |
INFO |
Esdeveniments normals del negoci | No |
WARNING |
Alguna cosa estranya que es va recuperar sola | No, però es vigila |
ERROR |
L'operació va fallar per a l'usuari | Sí |
CRITICAL |
El servei no pot continuar | Sí, urgent |
La distinció entre WARNING i ERROR és la que es fa malament més sovint: un reintent que després funciona és WARNING —l'usuari no se'n va assabentar—; un reintent esgotat que retorna un error al client és ERROR.
- L'explorador de registres i el seu llenguatge de consultes
L'explorador de registres és la interfície de consulta, i el seu llenguatge és senzill però té detalls que convé conèixer.
Els operadors bàsics:
resource.type="k8s_container" # igualtat exacta severity>=ERROR # comparació de gravetat jsonPayload.importe_eur>100 # comparació numèrica jsonPayload.mensaje:"pasarela" # ':' és CONTÉ, no igualtat jsonPayload.causa=~"timeout.*" # expressió regular timestamp>="2026-08-05T20:00:00Z" # rang temporal resource.type="k8s_container" AND severity=ERROR # combinació NOT jsonPayload.ruta="/salud" # negació
| Operador | Significat | Nota important |
|---|---|---|
= |
Igualtat exacta | Distingeix majúscules |
: |
Conté | Cerca de subcadena |
=~ / !~ |
Coincideix / no coincideix amb regex | Més lent |
>=, <=, >, < |
Comparació | Números, dates i severitats |
AND, OR, NOT |
Lògics | Usa parèntesis per agrupar |
- al davant |
Exclou | -severity=INFO |
Tres consells de rendiment que canvien molt l'experiència, perquè una consulta mal escrita sobre setmanes de registres triga minuts:
- Acota sempre el temps primer. És el filtre més eficient amb diferència.
- Filtra per
resource.typeaviat. Redueix l'espai de cerca de cop. - Evita la cerca global de text lliure si pots filtrar per un camp concret: cercar a
jsonPayload.causaés ordres de magnitud més ràpid que cercar la paraula en tot el contingut.
Des de la línia de comandes, per a guions i per automatitzar:
gcloud logging read \
'resource.type="k8s_container"
AND resource.labels.namespace_name="tienda"
AND severity>=ERROR
AND timestamp>="2026-08-05T20:00:00Z"' \
--limit=50 --format=json --project=alpinashop-prod
- Consultes que resolen incidents reals d'AlpinaShop
La teoria és curta; l'útil són els patrons. Aquestes cinc consultes cobreixen la majoria de les investigacions reals.
Consulta 1 — Els errors 500 des de l'últim desplegament. La primera pregunta davant de qualsevol incident:
resource.type="k8s_container" resource.labels.namespace_name="tienda" severity>=ERROR timestamp>="2026-08-05T20:15:00Z"
I refinada, per veure si el problema és d'una versió concreta —el que enllaça amb el $COMMIT_SHA de 06-01—:
resource.type="k8s_container" resource.labels.namespace_name="tienda" severity>=ERROR labels.version="a3f9c1b"
Consulta 2 — Aïllar la petició d'un client que s'ha queixat. Un client escriu dient que la seva compra va fallar a les 20:34:
resource.type="k8s_container" jsonPayload.id_cliente_hash="a3f9c1e8b2..." timestamp>="2026-08-05T20:30:00Z" timestamp<="2026-08-05T20:40:00Z"
Aquí es veu el valor de la regla de les dades personals: el hash permet trobar el client sense haver escrit mai el seu correu en un registre. Atenció al client converteix el correu en hash amb la mateixa funció, i la investigació funciona igual.
Consulta 3 — Qui va esborrar un recurs. Els registres d'auditoria, que responen a la pregunta més incòmoda:
logName="projects/alpinashop-prod/logs/cloudaudit.googleapis.com%2Factivity" protoPayload.methodName="v1.compute.firewalls.delete" timestamp>="2026-08-01T00:00:00Z"
La resposta inclou protoPayload.authenticationInfo.principalEmail —qui— i protoPayload.resourceName —què—. És la consulta que resol en trenta segons discussions que d'una altra manera duren una tarda.
Consulta 4 — Peticions lentes del balancejador amb el seu origen. Sobre el registre d'accés de l'apartat 2:
I el mateix registre respon una pregunta de cost de 03-03, la ràtio d'encerts de la CDN:
resource.type="http_load_balancer" jsonPayload.cacheId!="" jsonPayload.statusDetails="response_from_cache"
Consulta 5 — Errors de la funció d'imatges. Tancant el cercle amb 06-03:
resource.type="cloud_run_revision" resource.labels.service_name="procesar-imagen-producto" severity>=ERROR
I la que hauria detectat el bucle infinit de l'exercici d'aquella lliçó, comptant invocacions per hora:
resource.type="cloud_run_revision" resource.labels.service_name="procesar-imagen-producto" jsonPayload.message:"Procesada correctamente"
- Buckets, retenció, vistes i àmbits
Els registres s'emmagatzemen en buckets de registres, que no tenen res a veure amb els de Cloud Storage.
Tot projecte en té dos per defecte:
| Bucket | Contingut | Retenció per defecte | Configurable? |
|---|---|---|---|
_Required |
Registres d'auditoria d'activitat d'administració | 400 dies | No, i és gratuït |
_Default |
Tota la resta | 30 dies | Sí |
I es poden crear buckets propis, que és el que cal quan diferents tipus de registre necessiten un tractament diferent:
# Un bucket amb retenció llarga per al que està relacionat amb pagaments
gcloud logging buckets create logs-pagos \
--location=europe-west1 \
--retention-days=2555 \
--description="Registres de transaccions - retenció 7 anys per normativa" \
--project=alpinashop-prod
# I un altre amb retenció curta per a registres de depuració de desenvolupament
gcloud logging buckets create logs-debug \
--location=europe-west1 \
--retention-days=7 \
--project=alpinashop-devTriar la retenció té dues dimensions que convé separar:
| Necessitat | Retenció | On |
|---|---|---|
| Diagnosticar un incident | 7-30 dies | Bucket de registres |
| Analitzar tendències | Mesos | Exportat a BigQuery |
| Compliment normatiu | Anys | Exportat a Cloud Storage |
Guardar anys de registres en un bucket de registres és la forma més cara de conservar-los. Per a retenció llarga, el patró correcte és exportar a Cloud Storage amb classe d'emmagatzematge freda, que és el tema de l'apartat següent.
Les vistes de registre (log views) permeten donar accés a un subconjunt d'un bucket. És la peça que resol un problema real de permisos: donar a la Lucía accés als registres de l'aplicació sense exposar-li els registres d'auditoria ni els de pagaments.
gcloud logging views create vista-catalogo \
--bucket=_Default --location=global \
--log-filter='resource.type="k8s_container" AND resource.labels.namespace_name="tienda"' \
--project=alpinashop-prod
gcloud logging views add-iam-policy-binding vista-catalogo \
--bucket=_Default --location=global \
--member='group:[email protected]' \
--role=roles/logging.viewAccessor \
--project=alpinashop-prodSense vistes, el permís de lectura de registres és tot o res, i «tot» inclou els registres d'auditoria. Amb vistes, l'accés s'acota, coherent amb el mínim privilegi de 03-04.
- Sortidors: exportar a BigQuery, Cloud Storage i Pub/Sub
Un sortidor (sink) és una regla que diu: «les entrades que compleixin aquest filtre, envia-les també a aquest destí». És el mecanisme que connecta els registres amb la resta de la plataforma.
flowchart LR
A[Cloud Logging<br/>encaminador de registres] --> B{Filtres dels<br/>sortidors}
B -->|registres d accés| C[BigQuery<br/>anàlisi SQL]
B -->|tot, comprimit| D[Cloud Storage<br/>arxiu barat]
B -->|errors crítics| E[Pub/Sub<br/>reacció automàtica]
B -->|per defecte| F[Bucket _Default<br/>30 dies]
A BigQuery, per analitzar. El cas d'AlpinaShop: analitzar el registre d'accés del balancejador amb SQL, creuant-lo amb les dades d'alpinashop_analitica:
gcloud logging sinks create sumidero-acceso-bq \
bigquery.googleapis.com/projects/alpinashop-datos/datasets/logs_acceso \
--log-filter='resource.type="http_load_balancer"' \
--use-partitioned-tables \
--project=alpinashop-prod
# El sortidor crea un compte de servei propi al qual cal donar permís
SA=$(gcloud logging sinks describe sumidero-acceso-bq \
--project=alpinashop-prod --format='value(writerIdentity)')
gcloud projects add-iam-policy-binding alpinashop-datos \
--member="$SA" --role=roles/bigquery.dataEditorEl pas del writerIdentity s'oblida constantment: el sortidor es crea, sembla funcionar, i no arriba res al destí perquè falta el permís. És el primer lloc on mirar si un sortidor no lliura.
El --use-partitioned-tables és important per cost: particiona per data, així que una consulta sobre un dia no escaneja mesos, aplicant el que s'ha après a 04-01.
I aleshores es poden fer anàlisis que amb l'explorador serien impossibles:
-- Top 20 d'URLs més lentes de l'última setmana, amb volum
SELECT
httpRequest.requestUrl AS url,
COUNT(*) AS peticiones,
ROUND(APPROX_QUANTILES(
CAST(REGEXP_EXTRACT(httpRequest.latency, r'([\d.]+)') AS FLOAT64), 100)[OFFSET(95)], 3
) AS latencia_p95_s,
COUNTIF(httpRequest.status >= 500) AS errores_5xx
FROM `alpinashop-datos.logs_acceso.requests_*`
WHERE _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
AND FORMAT_DATE('%Y%m%d', CURRENT_DATE())
GROUP BY url
HAVING peticiones > 100
ORDER BY latencia_p95_s DESC
LIMIT 20;A Cloud Storage, per arxivar barat. El destí de la retenció llarga:
gcloud logging sinks create sumidero-archivo-gcs \
storage.googleapis.com/alpinashop-logs-archivo \
--log-filter='logName:"cloudaudit.googleapis.com" OR jsonPayload.componente="checkout"' \
--project=alpinashop-prodAmb una regla de cicle de vida al bucket (02-02) que baixi a Nearline als 30 dies, a Coldline als 90 i a Archive a l'any, set anys de registres de pagaments costen una fracció minúscula del que costarien en un bucket de registres.
A Pub/Sub, per reaccionar. El sortidor més interessant conceptualment, perquè tanca el cercle amb 06-03:
gcloud logging sinks create sumidero-seguridad-pubsub \
pubsub.googleapis.com/projects/alpinashop-prod/topics/eventos-seguridad \
--log-filter='protoPayload.methodName=~"compute.firewalls.(insert|patch|delete)"
OR protoPayload.methodName="SetIamPolicy"' \
--project=alpinashop-prodCada vegada que algú toca una regla de tallafoc o una política d'IAM, arriba un missatge al topic, i una Cloud Function de 06-03 pot publicar-ho al canal de seguretat de Slack. D'un registre a una reacció automàtica, sense intervenció humana.
| Destí | Latència | Cost | Per a què |
|---|---|---|---|
| BigQuery | Segons | Emmagatzematge + consultes | Analitzar amb SQL |
| Cloud Storage | Minuts (per lots) | El més barat | Arxiu, compliment |
| Pub/Sub | Segons | Per missatge | Reaccionar en temps real |
| Un altre bucket de registres | Immediata | Ingesta | Retenció diferent per tipus |
| Un altre projecte | Segons | Ingesta | Agregació centralitzada (07-07) |
- Filtres d'exclusió: no pagar per soroll
Un sortidor copia; un filtre d'exclusió descarta. I és la palanca més directa sobre la factura de logging.
El raonament és senzill: hi ha registres que es generen en volum enorme i no aporten res al diagnòstic. A AlpinaShop, els principals són les comprovacions d'estat —hc-catalogo colpeja /salud cada pocs segons des de diverses sondes, generant desenes de milers d'entrades idèntiques al dia— i les peticions a recursos estàtics que serveix la CDN.
# Excloure les comprovacions d'estat del registre del balancejador
gcloud logging sinks update _Default \
--add-exclusion=name=excluir-health-checks,\
filter='resource.type="http_load_balancer" AND httpRequest.requestUrl:"/salud"' \
--project=alpinashop-prod
# Excloure el 95 % dels accessos correctes a estàtics, conservant una mostra
gcloud logging sinks update _Default \
--add-exclusion=name=excluir-estaticos,\
filter='resource.type="http_load_balancer"
AND httpRequest.status=200
AND httpRequest.requestUrl=~"\.(css|js|png|jpg|webp|woff2)$"',\
percent=95 \
--project=alpinashop-prodEl paràmetre percent=95 és molt útil i poc conegut: exclou una mostra aleatòria en lloc de tot. Conservar el 5 % permet continuar veient tendències i detectar problemes amb els estàtics, pagant-ne la vintena part.
| Candidat a excloure | Volum típic | Risc d'excloure'l |
|---|---|---|
| Comprovacions d'estat | Molt alt | Cap: les mètriques ja les cobreixen |
| Estàtics amb codi 200 | Alt | Baix, si conserves una mostra |
DEBUG en producció |
Alt | Cap: no hauria d'estar actiu |
Registres d'alpinashop-dev |
Mitjà | Baix: retenció curta és suficient |
| Errors de qualsevol tipus | Baix | No els excloguis mai |
| Registres d'auditoria | Mitjà | Mai: obligació normativa |
I l'advertiment imprescindible: una entrada exclosa no es desa enlloc i no es pot recuperar. Si exclous alguna cosa que després resulta necessària per investigar un incident, no hi ha marxa enrere. Revisa cada exclusió amb la pregunta: podria necessitar això durant una investigació? Davant del dubte, mostreja en lloc d'excloure del tot.
- Mètriques basades en registres
Aquí es tanca el vincle amb 06-04. Una mètrica basada en registres converteix entrades que compleixen un filtre en una mètrica de Cloud Monitoring, sense tocar el codi de l'aplicació.
És la tècnica més ràpida per instrumentar alguna cosa que ja s'està registrant.
Mètrica de comptador, per comptar ocurrències:
gcloud logging metrics create pagos_fallidos \
--description="Pagaments que fallen per timeout de la passarel·la" \
--log-filter='resource.type="k8s_container"
AND jsonPayload.causa="timeout_pasarela"' \
--project=alpinashop-prodAmb etiquetes per poder desglossar, definida des d'un fitxer:
# metrica-pagos-fallidos.yaml
name: pagos_fallidos
description: Pagaments que fallen per timeout de la passarel·la
filter: |
resource.type="k8s_container"
AND jsonPayload.causa="timeout_pasarela"
labelExtractors:
canal: EXTRACT(jsonPayload.canal)
version: EXTRACT(labels.version)
metricDescriptor:
metricKind: DELTA
valueType: INT64
labels:
- key: canal
- key: versionFixa't en l'etiqueta version: permet respondre immediatament a «aquestes fallades van començar amb el desplegament d'ahir?», correlacionant amb el SHA de 06-01. I respecta la regla de cardinalitat de 06-04: canal té tres valors, version unes desenes. Mai extreguis id_pedido com a etiqueta.
Mètrica de distribució, per a valors numèrics:
name: importe_pedidos
description: Distribució de l'import de les comandes completades
filter: |
resource.type="k8s_container"
AND jsonPayload.evento="pedido_completado"
valueExtractor: EXTRACT(jsonPayload.importe_eur)
metricDescriptor:
metricKind: DELTA
valueType: DISTRIBUTIONAixò permet pintar el p50 i el p95 de l'import mitjà de comanda, una dada de negoci pura obtinguda sense escriure una línia d'instrumentació específica.
| Enfocament | Avantatge | Inconvenient |
|---|---|---|
| Mètrica basada en registres | Sense tocar el codi, immediata | Depèn del format del registre; si canvia, es trenca |
| Mètrica personalitzada (06-04) | Explícita, robusta | Requereix codi i desplegament |
La recomanació pràctica: comença amb mètriques basades en registres per validar ràpid què val la pena mesurar, i promociona a mètriques personalitzades les que resultin importants de veritat. I tingues present el risc: si algú reanomena un camp del jsonPayload, la mètrica deixa de comptar silenciosament i l'alerta associada deixa de saltar. Un canvi en el format d'un registre és un canvi amb conseqüències, i mereix menció en la revisió de codi.
- Cloud Trace: què és el rastreig distribuït
Les mètriques diuen que el p95 és de 3,2 segons. Els registres diuen que hi va haver errors. Cap dels dos diu on van anar aquests 3,2 segons.
Aquest és el buit que omple el rastreig distribuït, i la seva necessitat creix amb el nombre de peces. Una petició al catàleg d'AlpinaShop travessa avui: el balancejador, Cloud Armor, la CDN, el pod de GKE, una consulta a Cloud SQL, una lectura de Firestore per al carret i potser una crida a la Vision API. Si triga tres segons, qui en té la culpa?
Dos conceptes:
- Una traça representa una petició completa a través de tot el sistema, identificada per un
trace_id. - Un tram (span) representa una operació dins d'aquesta traça, amb inici, fi i un tram pare. Els trams formen un arbre.
flowchart TD
A["GET /catalogo/piolet-01<br/>3.240 ms — tram arrel"] --> B["ficha_producto<br/>3.200 ms"]
A --> C["render_plantilla<br/>40 ms"]
B --> D["SELECT productos<br/>40 ms"]
B --> E["SELECT stock_por_talla<br/>3.020 ms — 93% del total"]
B --> F["recomendaciones<br/>20 ms"]
Amb només mirar el diagrama, la resposta salta a la vista: SELECT stock_por_talla triga 3.020 dels 3.240 ms. Cap mètrica i cap registre haurien assenyalat això amb aquesta claredat.
La propagació del context és el que permet que els trams de serveis diferents formin una sola traça. El servei que inicia la petició genera un trace_id i l'envia en una capçalera; cada servei que la rep la llegeix, crea els seus trams com a fills i la propaga al seu torn.
| Capçalera | Origen | Format | Estat el 2026 |
|---|---|---|---|
traceparent |
Estàndard W3C | 00-{trace_id}-{span_id}-{flags} |
La recomanada |
X-Cloud-Trace-Context |
{trace_id}/{span_id};o={flags} |
Nativa de GCP, encara molt present | |
b3 / X-B3-TraceId |
Zipkin | Diversos | Heretada |
El balancejador de GCP afegeix X-Cloud-Trace-Context automàticament a cada petició entrant, així que AlpinaShop ja té identificadors de traça circulant sense haver fet res. El que falta és que l'aplicació els faci servir.
- Instrumentar Flask amb OpenTelemetry
OpenTelemetry és l'estàndard del sector per a instrumentació —mètriques, traces i registres—, independent del proveïdor. Instrumentar amb OpenTelemetry i exportar a Cloud Trace significa que, si demà AlpinaShop canvia de destí, es canvia l'exportador i no la instrumentació.
# requirements.txt opentelemetry-api==1.* opentelemetry-sdk==1.* opentelemetry-instrumentation-flask==0.* opentelemetry-instrumentation-requests==0.* opentelemetry-instrumentation-sqlalchemy==0.* opentelemetry-exporter-gcp-trace==1.* opentelemetry-propagator-gcp==1.*
# catalogo/trazas.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBased
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.cloud_trace_propagator import CloudTraceFormatPropagator
import os
def configurar_trazas(app, engine):
"""Instrumenta l'aplicació Flask i exporta a Cloud Trace."""
# Mostreig: ParentBased respecta la decisió del servei que va originar
# la traça; si és el primer, mostreja segons la proporció indicada.
proporcion = float(os.environ.get("TRACE_SAMPLE_RATE", "0.1"))
proveedor = TracerProvider(sampler=ParentBased(TraceIdRatioBased(proporcion)))
# BatchSpanProcessor agrupa els trams i els envia en lots:
# imprescindible per no afegir latència a cada petició.
proveedor.add_span_processor(BatchSpanProcessor(CloudTraceSpanExporter()))
trace.set_tracer_provider(proveedor)
# Usar el format de Google per entendre's amb el balancejador
set_global_textmap(CloudTraceFormatPropagator())
# Instrumentació automàtica: trams sense tocar el codi de negoci
FlaskInstrumentor().instrument_app(app) # un tram per petició
RequestsInstrumentor().instrument() # un tram per crida HTTP sortint
SQLAlchemyInstrumentor().instrument(engine=engine) # un tram per consulta SQLAmb aquestes tres últimes línies ja tens traces útils sense modificar cap funció de negoci. Per al detall propi, trams manuals:
# catalogo/producto.py
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def ficha_producto(sku):
with tracer.start_as_current_span("ficha_producto") as span:
span.set_attribute("sku", sku) # atributs de BAIXA cardinalitat
with tracer.start_as_current_span("consultar_stock"):
stock = consultar_stock_por_talla(sku)
span.set_attribute("tallas_disponibles", len(stock))
with tracer.start_as_current_span("recomendaciones"):
recos = obtener_recomendaciones(sku) # taula de 05-07
return render(sku, stock, recos)I la peça que ho canvia tot: incloure el trace_id als registres. És el que permetrà el recorregut de l'apartat 15:
# catalogo/registro.py
from opentelemetry import trace
def campos_traza() -> dict:
"""Retorna els camps que enllacen un registre amb la seva traça."""
span = trace.get_current_span()
ctx = span.get_span_context()
if not ctx.is_valid:
return {}
proyecto = os.environ["GOOGLE_CLOUD_PROJECT"]
return {
"logging.googleapis.com/trace": f"projects/{proyecto}/traces/{ctx.trace_id:032x}",
"logging.googleapis.com/spanId": f"{ctx.span_id:016x}",
"logging.googleapis.com/trace_sampled": ctx.trace_flags.sampled,
}
def log_info(mensaje: str, **campos):
log.info(mensaje, extra={"json_fields": {**campos, **campos_traza()}})Aquestes claus especials logging.googleapis.com/trace i .../spanId no són camps qualssevol: Cloud Logging les reconeix i emplena els camps trace i spanId de l'entrada. I amb això, la consola mostra un enllaç directe de cada registre a la seva traça, i de cada traça als seus registres.
- Llegir una cascada i trobar la consulta lenta
Amb la instrumentació posada, així es llegeix una traça lenta del catàleg d'AlpinaShop:
| Tram | Durada | % del total | Observació |
|---|---|---|---|
GET /catalogo/piolet-01 |
3.240 ms | 100 % | El tram arrel |
├─ ficha_producto |
3.200 ms | 99 % | Gairebé tot el temps |
│ ├─ SELECT productos |
40 ms | 1 % | Normal |
│ ├─ SELECT stock_por_talla |
3.020 ms | 93 % | El culpable |
│ └─ recomendaciones |
20 ms | 1 % | La taula precalculada de 05-07 |
└─ render_plantilla |
40 ms | 1 % | Normal |
El diagnòstic és immediat, i les tres coses que cal mirar en qualsevol cascada són sempre les mateixes:
- Quin tram ocupa el percentatge més gran? Aquí,
stock_por_tallaamb el 93 %. Optimitzar qualsevol altra cosa és perdre el temps. - Hi ha buits sense cobrir? Un interval dins del tram pare que cap fill no explica sol ser temps d'espera: bloquejos, contenció de pool de connexions o codi no instrumentat.
- Hi ha trams repetits? Cinquanta trams
SELECTidèntics consecutius són la signatura inconfusible del problema N+1: una consulta per cada element d'una llista, en lloc d'una consulta per a tots.
Amb l'atribut SQL que afegeix la instrumentació de SQLAlchemy, la traça inclou la consulta:
SELECT s.talla, s.unidades
FROM stock s
WHERE s.sku = 'piolet-01'
AND s.almacen IN (SELECT id FROM almacenes WHERE activo = true);I a partir d'aquí, la investigació és de base de dades: EXPLAIN ANALYZE sobre aquesta consulta, revisar índexs, mirar els registres de consultes lentes de Cloud SQL. La traça no arregla el problema; el localitza en trenta segons en lloc de en tres hores, i això és exactament el que se li demana.
Un patró que mereix menció a part: si aquesta mateixa consulta triga 40 ms en un entorn de proves i 3.020 ms en producció, el problema probablement no és la consulta sinó la contenció —el pool de connexions esgotat, amb la petició esperant que se'n alliberi una—. Allà la mètrica de connexions de Cloud SQL de 06-04 i la traça es reforcen mútuament: la traça diu on s'espera, la mètrica diu per què.
- Mostreig i cost de les traces
Traçar el 100 % de les peticions té dos costos: el d'ingesta de trams, que es factura, i el de rendiment a l'aplicació, que encara que petit no és nul. Per això es mostreja.
| Estratègia | Com funciona | Quan |
|---|---|---|
| Proporció fixa | Un percentatge de les traces | L'habitual: 1-10 % en producció |
| Basada en el pare | Respecta la decisió del primer servei | Sempre, combinada amb l'anterior |
| Sempre actiu | 100 % | Desenvolupament i depuració puntual |
| Basada en la cua | Decideix en acabar, segons el resultat | Ideal, requereix col·lector propi |
La combinació ParentBased(TraceIdRatioBased(0.1)) de l'apartat 11 és la correcta per defecte, i mereix explicació: TraceIdRatioBased(0.1) mostreja el 10 % de les traces noves, i ParentBased garanteix que si una traça es va mostrejar al principi, tots els serveis que la reben també la mostregen. Sense ParentBased, cada servei decidiria pel seu compte i obtindries traces incompletes amb forats, que són pitjors que no tenir traces.
El mostreig basat en la cua és el que tothom voldria —guardar el 100 % de les traces lentes o amb error, i l'1 % de les normals— perquè les traces interessants són precisament les anòmales. Requereix un col·lector d'OpenTelemetry que retingui els trams fins al final de la petició per decidir. Per a AlpinaShop avui és complexitat excessiva; convé saber que existeix per quan el sistema creixi.
Recomanació per entorn:
| Entorn | Proporció | Motiu |
|---|---|---|
alpinashop-dev |
100 % | Volum baix, es vol veure tot |
alpinashop-prod normal |
5-10 % | Suficient per detectar patrons |
| Durant un incident | Pujar temporalment | Per això és una variable d'entorn |
Que la proporció sigui una variable d'entorn (TRACE_SAMPLE_RATE) és deliberat: permet pujar-la al 100 % durant una investigació sense redesplegar codi, i baixar-la després.
- Cloud Profiler: perfilatge continu
Una traça diu que una funció triga 800 ms. No diu què fa durant aquests 800 ms. Per a això hi ha el perfilatge.
Cloud Profiler recull contínuament mostres de CPU i memòria de l'aplicació en producció, amb una sobrecàrrega molt baixa —de l'ordre d'un percentatge petit—, i mostra un gràfic de flames amb on es consumeix el temps i la memòria.
# catalogo/app.py, al principi de l'arrencada
import googlecloudprofiler
try:
googlecloudprofiler.start(
service="catalogo-web",
service_version=os.environ["VERSION_IMAGEN"], # el SHA de 06-01
verbose=0,
)
except (ValueError, NotImplementedError) as e:
log.warning("No s'ha pogut iniciar el profiler: %s", e)Els tres casos on Profiler resol el que cap altre pilar no pot:
| Cas | Símptoma | El que revela |
|---|---|---|
| Fuita de memòria | El pod es reinicia cada poques hores per OOM | Quina estructura creix sense alliberar-se |
| CPU alta sense causa clara | La mètrica diu 85 %, la traça no assenyala res | Quina funció concreta consumeix |
| Optimitzar el que importa | Es vol millorar el rendiment | On se'n va el temps de veritat |
El tercer mereix un advertiment: la intuïció sobre on és el coll d'ampolla sol ser errònia. És habitual passar dos dies optimitzant una funció que consumeix el 3 % del temps total mentre el 60 % se l'endú una serialització JSON que ningú no va mirar. Profiler evita aquest malbaratament amb dades en lloc de conjectures.
I la comparació per versió és on brilla: amb service_version fixat al SHA de la imatge, es pot comparar el perfil de dues versions i veure exactament què va introduir una regressió de rendiment.
- La correlació que ho uneix tot: un incident de principi a fi
Aquest apartat és la raó de ser de tot el mòdul. Tornem al moment amb què va començar la lliçó.
flowchart TD
A[23:14 ALERTA<br/>Taxa 5xx > 2%] --> B[23:15 TAULER<br/>4,3% errors, p95 3,2s]
B --> C[23:17 REGISTRES<br/>severity=ERROR<br/>últims 30 min]
C --> D[23:19 Un patró:<br/>causa=timeout_consulta<br/>a /catalogo]
D --> E[23:21 TRAÇA<br/>des del trace del registre]
E --> F[23:23 CASCADA<br/>SELECT stock_por_talla<br/>3.020 ms]
F --> G[23:25 CAUSA<br/>índex eliminat<br/>a la migració de les 22:50]
G --> H[23:31 MITIGACIÓ<br/>marxa enrere amb 06-01]
23:14 — L'alerta. Arriba al mòbil de la Marta. Porta el camp documentation amb els quatre primers passos, així que no comença de zero.
23:15 — El tauler. Ho confirma: 4,3 % d'errors, p95 de 3,2 s davant dels 400 ms habituals, i la CPU del MIG baixa. Aquesta última dada és informativa per si sola: si hi hagués un pic de trànsit, la CPU estaria alta. No ho està, així que el problema no és de capacitat.
23:17 — Els registres. Primera consulta, la de l'apartat 5:
resource.type="k8s_container" resource.labels.namespace_name="tienda" severity>=ERROR timestamp>="2026-08-05T23:00:00Z"
Apareixen unes 800 entrades. La Marta en mira una:
{
"severity": "ERROR",
"jsonPayload": {
"mensaje": "Timeout consultant estoc",
"sku": "piolet-01",
"causa": "timeout_consulta",
"duracion_ms": 5001,
"id_cliente_hash": "a3f9c1..."
},
"labels": {"componente": "catalogo", "version": "a3f9c1b"},
"trace": "projects/alpinashop-prod/traces/4bf92f3577b34da6a3ce929d0e0e4736"
}23:19 — El patró. Refina la consulta agrupant per causa i descobreix que el 94 % dels errors tenen causa="timeout_consulta" i tots són de rutes de catàleg. No és una fallada general: és una operació concreta.
23:21 — La traça. I aquí hi ha el salt que justifica tota la instrumentació de l'apartat 11: a la consola, el camp trace d'aquesta entrada de registre és un enllaç. Un clic i apareix la traça completa d'aquesta petició exacta, la del client concret que va patir l'error.
23:23 — La cascada. La traça és la de l'apartat 12: SELECT stock_por_talla consumeix 3.020 de 3.240 ms. Amb la consulta SQL visible als atributs del tram.
23:25 — La causa. La Marta executa la consulta 3 sobre els registres d'auditoria i sobre l'historial de Cloud Build: a les 22:50 es va aplicar una migració de base de dades que, entre altres coses, va eliminar i recrear una taula auxiliar sense tornar a crear l'índex sobre (sku, almacen). Sense aquest índex, la consulta passa d'un accés indexat a un recorregut complet.
23:31 — La mitigació. Es recrea l'índex, i en paral·lel es prepara la marxa enrere de la versió amb el pipeline de 06-01. La latència torna a la normalitat en tres minuts.
Disset minuts des de l'alerta fins a la causa arrel identificada. Sense aquest recorregut, la mateixa investigació hauria estat: assabentar-se'n per un client l'endemà al matí, mirar el web, no reproduir el problema perquè el trànsit nocturn és diferent, revisar registres sense estructura cercant text, sospitar de tres coses equivocades i, amb sort, trobar-ho al final del dia.
| Peça | Què va aportar exactament |
|---|---|
| Alerta (06-04) | Assabentar-se'n en 5 minuts, no l'endemà al matí |
| Tauler (06-04) | Confirmar l'abast i descartar la manca de capacitat |
| Registres estructurats | Agrupar 800 errors en un patró, no llegir 800 línies |
jsonPayload |
Agrupar per causa sense analitzar text |
Camp trace |
El salt del registre a la traça: un clic |
| Traça | Localitzar el 93 % del temps en una consulta |
| Atribut SQL del tram | La consulta exacta, sense endevinar |
| Registres d'auditoria | Correlacionar amb el canvi de les 22:50 |
Etiqueta version |
Saber quina versió va introduir el problema |
| Pipeline (06-01) | Marxa enrere en minuts, amb una imatge coneguda |
La lliçó que cal endur-se: cap peça no serveix sola. Una alerta sense registres només produeix ansietat. Registres sense estructura són un abocador. Traces sense registres correlacionats obliguen a cercar a cegues quina mirar entre milions. El valor és en els enllaços entre les peces, i l'enllaç concret que fa possible tot el recorregut és una línia de codi: incloure el trace_id a cada entrada de registre.
- Cost per volum i les tres decisions que el controlen
El model és senzill: es paga per GB ingerit, amb un volum gratuït mensual. La retenció dins del període per defecte no es factura a part; ampliar-la, sí. I verifica sempre els preus vigents a la documentació oficial.
| Component | Es paga per | Nivell gratuït |
|---|---|---|
| Ingesta de registres | GB ingerits | Un volum mensual generós |
| Retenció ampliada | GB × mes per sobre del període per defecte | — |
| Registres d'auditoria d'administració | Res | Sempre gratuïts |
| Ingesta de traces | Trams ingerits | Un volum mensual |
| Profiler | Res | Inclòs |
| Sortidors a Cloud Storage o Pub/Sub | El destí, no l'encaminament | — |
Les tres decisions que controlen la factura, per ordre d'impacte:
Decisió 1: què s'ingereix. És la més important amb diferència, perquè afecta tota la resta. Els filtres d'exclusió de l'apartat 8 sobre comprovacions d'estat i estàtics solen retallar entre el 40 % i el 70 % del volum d'un lloc web típic, sense perdre res de valor diagnòstic. I a l'aplicació: DEBUG desactivat en producció i cap registre dins de bucles.
Decisió 2: quant de temps es guarda i on. Trenta dies al bucket de registres per a diagnòstic, i el que hagi de conservar-se més temps, exportat. Un sortidor a Cloud Storage amb cicle de vida a Coldline i Archive fa que set anys de registres de pagaments costin ordres de magnitud menys que ampliar la retenció del bucket.
Decisió 3: quant es mostreja. S'aplica al registre del balancejador (--logging-sample-rate), als VPC Flow Logs (--logging-flow-sampling) i a les traces (TRACE_SAMPLE_RATE). Amb volum alt, el 10 % sol bastar per detectar patrons, perquè l'estadística no necessita el cens complet.
I l'error que cal evitar per damunt de tots: excloure registres d'error per estalviar. Són un percentatge mínim del volum i el 100 % del valor durant un incident. L'estalvi és en el soroll repetitiu, mai en l'excepcional.
Una perspectiva final per calibrar: per a AlpinaShop, l'observabilitat completa —registres, traces, mètriques, alertes— és de l'ordre d'una fracció petita del cost de la infraestructura que observa. Comparat amb els disset minuts de l'apartat 15 davant d'un dia sencer d'investigació a cegues, i amb les comandes que se salven en detectar un incident en cinc minuts en lloc de en dotze hores, és de les inversions més rendibles de la plataforma.
Errors Habituals i Consells
Usar print en lloc de logging estructurat. El text acaba a Cloud Logging, sí, però sense severitat, sense camps consultables i sense trace. És la diferència entre poder investigar i no poder.
Ficar les dades al missatge en lloc de en camps. f"comanda {id} va fallar" obliga a analitzar text. "Comanda fallida" més {"id_pedido": id} permet filtrar i agregar.
Escriure dades personals als registres. Correus, noms, targetes, adreces. Els registres es retenen, s'exporten i els llegeix molta gent. Fes servir hashes estables.
Tot amb severitat INFO. Impedeix filtrar per gravetat i fa inútils les alertes basades en severity>=ERROR. I un reintent que funciona és WARNING, no ERROR.
Oblidar el writerIdentity en crear un sortidor. El sortidor es crea sense error i no lliura res. És el primer lloc on mirar quan un destí és buit.
Excloure registres sense pensar-s'ho dues vegades. L'exclòs no es desa enlloc i no es recupera. Davant del dubte, mostreja amb percent en lloc d'excloure del tot. I no excloguis mai errors ni registres d'auditoria.
Guardar anys de registres en un bucket de registres. És la forma més cara. Per a retenció llarga, sortidor a Cloud Storage amb cicle de vida.
Etiquetes d'alta cardinalitat en mètriques basades en registres. El mateix error de 06-04: EXTRACT(jsonPayload.id_pedido) crea una sèrie temporal per comanda. Extreu dimensions amb desenes de valors, no milers.
Traçar sense ParentBased al mostrejador. Cada servei decideix pel seu compte i obtens traces incompletes amb forats, que confonen més que no ajuden.
No incloure el trace_id als registres. És una línia de codi i és la peça que converteix tres eines independents en un sistema de diagnòstic. Sense ella, saltar del registre a la traça és impossible.
Optimitzar per intuïció en lloc de per perfil. Dos dies millorant una funció que consumeix el 3 % del temps. Mesura primer amb Profiler.
Consell final: la instrumentació es prova abans de l'incident. Provoca un error a propòsit a alpinashop-dev, cerca'n el registre, salta a la seva traça i comprova que el recorregut complet funciona. Descobrir a les onze de la nit que el camp trace és buit és descobrir-ho en el pitjor moment possible.
Exercicis
Exercici 1: redissenyar el logging d'un mòdul
Aquest és el codi real d'una funció del catàleg d'AlpinaShop:
def procesar_devolucion(id_pedido, email_cliente, motivo):
print("Processant devolució de la comanda " + str(id_pedido))
try:
pedido = obtener_pedido(id_pedido)
if pedido.estado != "entregado":
print("Comanda no lliurada, no es pot retornar: " + str(id_pedido))
return False
reembolso = calcular_reembolso(pedido)
ejecutar_reembolso(pedido, reembolso)
print("Devolució OK per a " + email_cliente + ", import " + str(reembolso))
return True
except Exception as e:
print("Error: " + str(e))
return FalseReescriu-lo amb logging estructurat corregint tots els problemes que detectis, justifica cada canvi, i escriu la consulta de l'explorador de registres que respongui a «quantes devolucions es van rebutjar per estat incorrecte aquesta setmana, i de quin import mitjà?».
Exercici 2: dissenyar l'estratègia de retenció i exportació
AlpinaShop genera aproximadament: 80 GB/mes de registres del balancejador, 15 GB/mes de registres de l'aplicació, 30 GB/mes de VPC Flow Logs, 2 GB/mes de registres d'auditoria d'administració i 5 GB/mes de registres de Cloud SQL. Requisits: els registres de transaccions de pagament s'han de conservar 7 anys per normativa; l'equip necessita 30 dies per a diagnòstic; la Lucía vol analitzar el registre d'accés amb SQL durant els últims 12 mesos; i cal reduir el cost tant com sigui possible sense perdre capacitat de diagnòstic. Dissenya la configuració completa de buckets, sortidors i exclusions, amb una estimació qualitativa de l'estalvi.
Exercici 3: diagnosticar amb una traça incompleta
Un client informa que la pàgina d'un producte triga 8 segons. En cercar la traça, la Marta troba: tram arrel GET /catalogo/crampon-02 de 8.100 ms; a dins, consultar_producto de 120 ms i render_plantilla de 90 ms; i res més. Els 7.890 ms restants no estan coberts per cap tram fill. Els registres d'aquesta petició només mostren una entrada INFO al principi. Explica què significa aquest buit, enumera almenys quatre hipòtesis amb el que les distingiria, i detalla quina instrumentació afegiries perquè aquest cas sigui diagnosticable la propera vegada.
Solucions
Solució 1
Problemes del codi original, set en total:
| Problema | Gravetat | Conseqüència |
|---|---|---|
print en lloc de logging |
Alta | Sense severitat, sense camps, sense trace |
email_cliente al registre |
Crítica | Fuita de dades personals, RGPD |
| Dades concatenades al missatge | Alta | Impossible filtrar o agregar |
Tot amb severitat implícita INFO |
Alta | Un error no es distingeix d'un esdeveniment normal |
except Exception sense traceback |
Alta | Es perd on va fallar |
| Sense context a l'error | Alta | «Error: X» no diu de quina comanda |
| Rebuig registrat com a text | Mitjana | No es pot comptar ni analitzar |
Versió reescrita:
from catalogo.registro import log, campos_traza
import hashlib
def hash_cliente(email: str) -> str:
"""Hash estable: permet correlacionar sense identificar."""
return hashlib.sha256(email.lower().encode()).hexdigest()[:16]
def procesar_devolucion(id_pedido, email_cliente, motivo):
# Context comú a totes les entrades d'aquesta operació
contexto = {
"id_pedido": str(id_pedido),
"id_cliente_hash": hash_cliente(email_cliente), # MAI el correu
"motivo": motivo,
"operacion": "devolucion",
**campos_traza(), # enllaç amb la traça
}
log.info("Devolució iniciada", extra={"json_fields": contexto})
try:
pedido = obtener_pedido(id_pedido)
if pedido.estado != "entregado":
# WARNING, no ERROR: el sistema funciona, és una regla de negoci
log.warning("Devolució rebutjada", extra={"json_fields": {
**contexto,
"resultado": "rechazada",
"causa": "estado_incorrecto",
"estado_pedido": pedido.estado,
"importe_pedido_eur": float(pedido.importe),
}})
return False
reembolso = calcular_reembolso(pedido)
ejecutar_reembolso(pedido, reembolso)
log.info("Devolució completada", extra={"json_fields": {
**contexto,
"resultado": "completada",
"importe_reembolso_eur": float(reembolso),
"dias_desde_entrega": (hoy() - pedido.fecha_entrega).days,
}})
return True
except PedidoNoEncontrado:
log.error("Devolució fallida: comanda inexistent",
extra={"json_fields": {**contexto, "resultado": "error",
"causa": "pedido_no_encontrado"}})
return False
except ErrorPasarelaReembolso as e:
# log.exception inclou el traceback complet automàticament
log.exception("Devolució fallida: error de la passarel·la",
extra={"json_fields": {**contexto, "resultado": "error",
"causa": "error_pasarela",
"codigo_pasarela": e.codigo}})
raise # es rellança: algú se n'ha d'assabentar
except Exception:
log.exception("Devolució fallida: error inesperat",
extra={"json_fields": {**contexto, "resultado": "error",
"causa": "desconocida"}})
raiseJustificació dels canvis més importants:
- El hash del correu resol la fuita sense perdre capacitat d'investigació: atenció al client aplica la mateixa funció i troba les entrades.
WARNINGper al rebuig per estat. És una decisió deliberada: el sistema va fer el correcte, no hi ha avaria. Si fosERROR, l'alerta de 06-04 saltaria per devolucions perfectament normals, alimentant la fatiga de l'apartat 11 d'aquella lliçó.- El camp
resultadoamb valors acotats (completada,rechazada,error) permet construir un embut complet amb una sola mètrica basada en registres. - Excepcions específiques abans que la genèrica. Cadascuna registra la seva
causa, la qual cosa permet distingir un problema de la passarel·la d'un error de programació, que requereixen respostes molt diferents. raiseals errors reals. El codi original retornavaFalsetant si la comanda no era retornable com si la passarel·la estava caiguda, i això fa impossible distingir-los des de fora.campos_traza()a totes les entrades. El que permet el salt de l'apartat 15.
Consulta demanada:
resource.type="k8s_container" resource.labels.namespace_name="tienda" jsonPayload.operacion="devolucion" jsonPayload.resultado="rechazada" jsonPayload.causa="estado_incorrecto" timestamp>="2026-07-29T00:00:00Z"
Per a l'import mitjà, l'explorador compta però no fa mitjanes bé. Dues opcions. La ràpida, una mètrica de distribució basada en registres sobre jsonPayload.importe_pedido_eur amb aquest filtre, que dona el p50 i la mitjana a Cloud Monitoring. La completa, un sortidor a BigQuery i SQL:
SELECT
COUNT(*) AS rechazadas,
ROUND(AVG(CAST(JSON_VALUE(jsonPayload.importe_pedido_eur) AS FLOAT64)), 2) AS importe_medio,
JSON_VALUE(jsonPayload.estado_pedido) AS estado
FROM `alpinashop-datos.logs_app.stdout_*`
WHERE _TABLE_SUFFIX BETWEEN '20260729' AND '20260805'
AND JSON_VALUE(jsonPayload.causa) = 'estado_incorrecto'
GROUP BY estado
ORDER BY rechazadas DESC;I una observació que va més enllà de l'exercici: aquest desglossament per estado_pedido és informació de producte, no d'infraestructura. Si resulta que la majoria dels rebutjos són de comandes en estat en_transito, la troballa no és tècnica: és que hi ha clients intentant retornar alguna cosa que encara no han rebut, i probablement la interfície no els ho està explicant bé. Un bon logging estructurat acaba responent preguntes de negoci que ningú no havia pensat a fer.
Solució 2
Volum total: 132 GB/mes. El registre del balancejador és el 61 % i els Flow Logs el 23 %: entre tots dos, el 84 %. Aquí hi ha tot el marge.
Pas 1 — Exclusions (la palanca principal):
| Exclusió | Filtre | Estalvi estimat |
|---|---|---|
| Comprovacions d'estat | httpRequest.requestUrl:"/salud" |
~15 GB/mes |
| Estàtics 200, 95 % mostrejat | Extensions d'assets, status=200 |
~30 GB/mes |
| Flow Logs a mostreig 0,25 | --logging-flow-sampling=0.25 |
~15 GB/mes |
DEBUG en producció |
severity=DEBUG |
~2 GB/mes |
Volum ingerit resultant: uns 70 GB/mes, prop de la meitat. I sense perdre capacitat de diagnòstic: les comprovacions d'estat ja estan cobertes per les mètriques de 06-04, els estàtics conserven una mostra del 5 % suficient per veure tendències, i el 25 % de Flow Logs continua detectant patrons de connectivitat.
Pas 2 — Buckets amb retenció diferenciada:
| Bucket | Contingut | Retenció | Motiu |
|---|---|---|---|
_Default |
Aplicació, LB, SQL, Flow Logs | 30 dies | Finestra de diagnòstic |
_Required |
Auditoria d'administració | 400 dies | Fix i gratuït |
logs-pagos |
jsonPayload.componente="checkout" |
30 dies | Compte, vegeu a sota |
La decisió clau, i és contraintuïtiva: el bucket logs-pagos no es configura amb 7 anys de retenció. Guardar set anys en un bucket de registres és l'opció més cara amb diferència. La normativa exigeix conservar, no exigeix que estiguin consultables a l'explorador de registres. Per això:
Pas 3 — Sortidors:
# 1. Compliment: pagaments a Cloud Storage, amb cicle de vida
gcloud logging sinks create sumidero-pagos-archivo \
storage.googleapis.com/alpinashop-logs-pagos \
--log-filter='jsonPayload.componente="checkout" OR jsonPayload.operacion="devolucion"' \
--project=alpinashop-prod
# 2. Anàlisi: registre d'accés a BigQuery, particionat
gcloud logging sinks create sumidero-acceso-bq \
bigquery.googleapis.com/projects/alpinashop-datos/datasets/logs_acceso \
--log-filter='resource.type="http_load_balancer"' \
--use-partitioned-tables \
--project=alpinashop-prod
# 3. Seguretat: canvis sensibles a Pub/Sub
gcloud logging sinks create sumidero-seguridad \
pubsub.googleapis.com/projects/alpinashop-prod/topics/eventos-seguridad \
--log-filter='protoPayload.methodName=~"firewalls\.(insert|patch|delete)" OR protoPayload.methodName="SetIamPolicy"' \
--project=alpinashop-prodAmb el cicle de vida del bucket d'arxiu: Nearline a 30 dies, Coldline a 90, Archive a 365, esborrat a 2.555 dies (7 anys). I a BigQuery, caducitat de partició a 365 dies per al requisit de la Lucía.
Estimació qualitativa de l'estalvi:
| Concepte | Abans | Després |
|---|---|---|
| Ingesta | 132 GB/mes | ~70 GB/mes |
| Retenció de pagaments | 7 anys en bucket de registres | Archive a Cloud Storage |
| Anàlisi de 12 mesos | Impossible sense retenció llarga | BigQuery particionat |
| Capacitat de diagnòstic | 30 dies complets | 30 dies complets |
L'estalvi gruixut ve de dos llocs: la meitat del volum ingerit i, sobretot, treure la retenció llarga del servei car al barat, on la diferència de preu per GB-mes entre un bucket de registres i Archive és de dos ordres de magnitud.
I tres advertiments que completen el disseny. Primer: mai excloure errors ni auditoria; són un percentatge mínim del volum i el 100 % del valor. Segon: verificar que el sortidor de pagaments captura tot el que la normativa exigeix abans de reduir la retenció del bucket, perquè si el filtre està malament, el requisit legal s'incompleix en silenci. Tercer: posar una alerta sobre el volum ingerit amb el de 06-04, per assabentar-se si algú introdueix un registre en un bucle abans que arribi la factura.
Solució 3
Què significa el buit. 7.890 ms dels 8.100 no estan coberts per cap tram. I això té un significat molt concret: està passant alguna cosa que la instrumentació no veu. Només hi ha dues famílies d'explicació —codi no instrumentat, o temps d'espera que no és una operació— i distingir-les és tot l'exercici.
És important notar una cosa: els dos trams que sí que existeixen són ràpids. Si el problema fos a la consulta o al renderitzat, es veuria. El temps se'n va en l'espai entre trams, i això és el que cal investigar.
Quatre hipòtesis, amb el que les distingeix:
| Hipòtesi | Què passa | Com distingir-la |
|---|---|---|
| 1. Crida externa no instrumentada | Una API de tercers, una memòria cau remota, un servei intern sense instrumentar | VPC Flow Logs: hi ha trànsit sortint en aquesta finestra? Revisar el codi cercant urllib, httpx o un altre client no cobert per RequestsInstrumentor |
| 2. Espera pel pool de connexions | La petició espera que s'alliberi una connexió a Cloud SQL | Mètrica de connexions (06-04): està al límit? El tram de la consulta és curt, però es va esperar molt abans de començar-lo |
| 3. Bloqueig per GIL, CPU o memòria | Una altra petició monopolitza el procés, o hi ha recol·lecció d'escombraries agressiva | Cloud Profiler: gràfic de flames en aquesta franja. Mètrica de CPU del pod i de memòria |
| 4. Espera de xarxa externa | Latència en la resposta al client, client lent o TCP amb problemes | Comparar total_latencies amb backend_latencies al registre del balancejador: si difereixen molt, el temps és fora de l'aplicació |
Quina és la més probable i per què. La hipòtesi 1, i per un raonament que convé explicitar: la instrumentació automàtica d'OpenTelemetry cobreix Flask, requests i SQLAlchemy. Si el codi usa qualsevol altre client —urllib3 directament, un SDK d'un proveïdor de pagaments, un client de Redis, la biblioteca de Firestore— aquestes crides són completament invisibles. I vuit segons amb un patró tan net fa olor de timeout d'una crida externa, molt probablement un configurat en 8 segons exactes.
La hipòtesi 2 és la segona candidata i té una signatura reconeixible: es manifesta sota càrrega i desapareix quan el trànsit baixa. Si el problema només passa en hores punta, és aquesta.
Instrumentació a afegir, per ordre de prioritat:
1. Instrumentar tots els clients sortints. La mesura que resol el cas:
# Afegir a catalogo/trazas.py
from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
URLLib3Instrumentor().instrument() # cobreix SDKs que no usen requests
RedisInstrumentor().instrument()
HTTPXClientInstrumentor().instrument()2. Un tram explícit al voltant de cada operació de negoci. La regla general: si una operació pot trigar, embolcalla-la en un tram. És barat i elimina buits per construcció:
def ficha_producto(sku):
with tracer.start_as_current_span("ficha_producto") as span:
span.set_attribute("sku", sku)
with tracer.start_as_current_span("obtener_conexion_bd"):
conn = pool.acquire() # ← L'ESPERA DEL POOL, ara visible
with tracer.start_as_current_span("consultar_producto"):
producto = consultar(conn, sku)
with tracer.start_as_current_span("consultar_valoraciones"):
valoraciones = api_valoraciones.obtener(sku) # ← la crida externaEl tram obtener_conexion_bd és especialment valuós perquè separa el temps d'espera del temps de treball, que és justament el que confon la hipòtesi 2 amb la 1.
3. Un registre en entrar i en sortir de cada operació llarga, amb duracion_ms. Encara que falti el tram, dues entrades de registre amb marca de temps acoten on se'n va anar el temps.
4. Timeouts explícits i agressius en tot client extern, i registre quan salten:
try:
resp = requests.get(url, timeout=(2, 3)) # connexió 2 s, lectura 3 s
except requests.Timeout:
log.warning("Timeout a l'API de valoracions", extra={"json_fields": {
"servicio": "api_valoraciones", "timeout_s": 3, **campos_traza()}})
valoraciones = [] # degradació elegant: la fitxa es mostra sense ellesAquesta quarta mesura és la més important a llarg termini, i va més enllà del diagnòstic. Sense timeout explícit, la majoria dels clients HTTP esperen indefinidament o el temps per defecte del sistema operatiu, que pot ser molt llarg. Un servei del qual depens i que es degrada arrossega el teu, i vuit segons d'espera és exactament això. Amb timeout curt i degradació elegant, una API de valoracions caiguda produeix una fitxa sense valoracions en 3 segons en lloc d'una pàgina que no carrega en 8.
I la lliçó general de l'exercici: un buit en una traça no és una fallada de la traça, és informació. T'està dient amb precisió que hi ha una part del sistema que no estàs observant. La reacció correcta no és desconfiar de l'eina, sinó preguntar-se què fa el codi en aquest interval que ningú no va instrumentar. En aquest cas, gairebé segur, esperar algú que no contesta.
Conclusió
AlpinaShop té els tres pilars de l'observabilitat complets, i —més important— té els enllaços entre ells.
Saps què és una entrada de registre estructurada i per què la diferència entre textPayload i jsonPayload és la diferència entre poder investigar i no poder. Coneixes tots els seus camps, i saps quin és el decisiu: trace, el que enllaça el registre amb la seva traça.
Saps d'on surten els registres automàticament i quins cal activar explícitament: els del balancejador —dels més valuosos que existeixen, amb la seva latència, el seu país i el seu resultat de CDN— i els VPC Flow Logs. I saps emetre'ls bé des de Flask, amb les cinc regles: les dades van en camps i no al missatge, la severitat ha de ser correcta, mai dades personals, context suficient per actuar i sense soroll. Amb el criteri de severitat que més es falla: un reintent que funciona és WARNING; un d'esgotat, ERROR.
Domines l'explorador de registres i el seu llenguatge —amb el : que és «conté» i no «igual»—, els tres consells de rendiment, i cinc patrons de consulta que resolen incidents reals: els errors des d'un desplegament, la petició d'un client concret localitzada pel seu hash, qui va esborrar un recurs segons els registres d'auditoria, les peticions lentes del balancejador i els errors de la funció d'imatges.
Coneixes els buckets de registres amb _Required i _Default, la retenció amb els seus tres horitzons —diagnòstic, anàlisi i compliment, cadascun al seu lloc— i les vistes que permeten donar accés a un subconjunt sense exposar els registres d'auditoria. Saps crear sortidors a BigQuery per analitzar amb SQL, a Cloud Storage per arxivar barat amb cicle de vida i a Pub/Sub per reaccionar amb una funció de 06-03, amb el writerIdentity que tothom oblida. I saps usar els filtres d'exclusió amb el percent que mostreja en lloc de descartar, amb l'advertiment que l'exclòs no es recupera mai i que els errors i l'auditoria no s'exclouen mai.
Tens les mètriques basades en registres —comptadors i distribucions— que instrumenten sense tocar el codi i alimenten les alertes de 06-04, amb l'etiqueta version que respon a «això va començar amb el desplegament d'ahir?» i la mateixa regla de cardinalitat de sempre.
Saps què és el rastreig distribuït: traça i tram, la propagació del context amb traceparent de W3C i X-Cloud-Trace-Context que el balancejador ja està afegint. Saps instrumentar Flask amb OpenTelemetry —estàndard, no propietari—, amb la instrumentació automàtica de Flask, requests i SQLAlchemy, trams manuals per al detall, i la peça clau: incloure el trace_id a cada registre. Saps llegir una cascada mirant les tres coses que importen —quin tram domina, si hi ha buits i si hi ha trams repetits que delatin un N+1—, i saps mostrejar amb ParentBased(TraceIdRatioBased(...)) per no acabar amb traces incompletes. Coneixes Cloud Profiler i els seus tres casos, amb l'advertiment que la intuïció sobre on és el coll d'ampolla sol fallar.
I tens el recorregut complet de l'apartat 15: de l'alerta a la mètrica, de la mètrica al registre, del registre a la traça i de la traça a la línia de codi, en disset minuts. Amb la lliçó que resumeix tot el mòdul: cap peça no serveix sola; el valor és en els enllaços, i l'enllaç que ho fa possible cap en una línia de codi.
Finalment, coneixes el cost i les tres decisions que el controlen —què s'ingereix, quant es guarda i on, quant es mostreja— amb l'error que mai no s'ha de cometre: estalviar excloent errors, que són el mínim del volum i el màxim del valor.
Mira ara on és AlpinaShop. El codi és a GitHub i es revisa. El pipeline construeix, prova i desplega sol. Les funcions reaccionen a esdeveniments. Hi ha taulers, alertes, comprovacions des de quatre continents, registres estructurats, traces i perfils. Quan alguna cosa falla, se sap en cinc minuts i es diagnostica en vint.
I tanmateix, tota la infraestructura que sosté això —la VPC, el balancejador, el clúster, les alertes que acabes de crear, els sortidors que acabes de configurar— continua existint perquè algú va executar les comandes correctes. A 06-05 vas veure el problema amb claredat, vas entendre què és la infraestructura com a codi, vas conèixer Deployment Manager i vas aprendre el procediment de migració. El que falta és l'eina.
A 06-07, l'última lliçó del mòdul, arriba Terraform: l'estat i per què no va mai a Git, el flux plan i apply, el codi real d'AlpinaShop en HCL, els mòduls, la importació de tot el creat a mà i el plan automàtic a cada pull request. I amb això, la infraestructura d'AlpinaShop deixa per fi de viure a l'historial d'un terminal.
Curs de Google Cloud Platform (GCP)
Mòdul 1: Introducció a Google Cloud Platform
- Què és Google Cloud Platform?
- Configuració del teu compte de GCP
- Descripció general de la consola de GCP
- Projectes, jerarquia de recursos i facturació
- Regions, zones i model de responsabilitat compartida
- Cloud Shell i la CLI de gcloud
Mòdul 2: Serveis principals de GCP
- Compute Engine: màquines virtuals a Google Cloud
- Cloud Storage: emmagatzematge d'objectes
- Cloud SQL: bases de dades relacionals gestionades
- App Engine: plataforma com a servei
- Google Kubernetes Engine (GKE)
- Bases de dades NoSQL: Firestore, Bigtable i Spanner
- Com triar el servei de còmput adequat
Mòdul 3: Xarxes i seguretat
- Xarxes VPC
- Balanceig de càrrega al núvol
- Cloud CDN
- Gestió d'identitat i accés (IAM)
- Cloud Armor
- Secrets i xifratge: Secret Manager i Cloud KMS
- Cloud DNS, certificats TLS i publicació segura de serveis
Mòdul 4: Dades i anàlisi
- BigQuery: el magatzem de dades analític
- Cloud Dataflow: processament de dades per lots i en temps real
- Cloud Dataproc: Spark i Hadoop gestionats
- Cloud Pub/Sub: missatgeria asíncrona
- Cloud Data Fusion: integració de dades sense codi
- Orquestració de pipelines amb Cloud Composer i Workflows
- Govern de les dades i taulers amb Dataplex i Looker Studio
Mòdul 5: Aprenentatge automàtic i IA
- Vertex AI: la plataforma d'aprenentatge automàtic de GCP
- AutoML: models a mida sense escriure codi
- TensorFlow a GCP: entrenament i servei de models
- API de llenguatge natural
- API de visió
- IA generativa a Vertex AI: models Gemini i incrustacions
- MLOps: del model al producte amb Vertex AI Pipelines
Mòdul 6: DevOps i monitoratge
- Cloud Build: integració contínua a GCP
- Cloud Source Repositories i gestió del codi font
- Cloud Functions: funcions sense servidor
- Cloud Monitoring (abans Stackdriver): mètriques, taulers i alertes
- Cloud Deployment Manager i infraestructura com a codi nativa
- Cloud Logging i Cloud Trace: registres, traces i diagnòstic
- Terraform a GCP: infraestructura com a codi a la pràctica
Mòdul 7: Temes avançats de GCP
- Híbrid i multinúvol amb Anthos
- Computació sense servidor amb Cloud Run
- Xarxes avançades: VPC compartida, aparellament i connectivitat híbrida
- Bones pràctiques de seguretat
- Gestió i optimització de costos
- Fiabilitat: SLO, alta disponibilitat i recuperació de desastres
- Govern a escala: organització, polítiques i auditoria
