📑 Índice del documento
- Trabajo Práctico Nº 2 —
Parte 3
- Observabilidad — OpenTelemetry: Collector + SDK + multi-backend (vendor-neutral)
- Requisitos, consideraciones y formato de entrega
- Contenidos del programa relacionados
- Conceptos clave de OpenTelemetry
- Práctica
- Hit #1 — Deploy del OpenTelemetry Operator
- Hit #2 — OpenTelemetryCollector en modo Agent (DaemonSet) leyendo logs
- Hit #3 — Fan-out simultáneo a Loki + Elasticsearch (el hit clave)
- Hit #4 — Reemplazar Promtail/Alloy + Fluent Bit por solo OTel Collector
- Hit #5 — Instrumentar el scraper con el SDK de OpenTelemetry
- Hit #6 — Bonus: Traces (opcional, +5 %)
- Cómo entregar
- Auto-verificación
previa a la entrega
- 1)
install.shcorre limpio sobre Loki + EFK ya instalados - 2) El operator + el collector están Running
- 3) El collector recibe logs del scraper
- 4) Mismo log_id en Loki Y en Elastic
- 5) Promtail/Alloy y Fluent Bit están en 0
- 6) El scraper exporta OTLP directo (no via filelog)
- 7) RBAC del collector OK
- 8)
gitleaksno detecta secrets
- 1)
- Common pitfalls
- Criterios de evaluación
- Material de apoyo
- Referencias y Bibliografía
Trabajo Práctico Nº 2 — Parte 3
Observabilidad — OpenTelemetry: Collector + SDK + multi-backend (vendor-neutral)
Fecha de Entrega: 09/05/2026
🧭 Esto es la Parte 3 de 4 sobre observabilidad.
- Parte 1 — Loki + Promtail/Alloy + Grafana ✅
- Parte 2 — EFK = Elasticsearch + Fluent Bit + Kibana ✅
- Parte 3 (acá) — OpenTelemetry Collector + SDK
- Parte 4 — Cierre: ADR comparativo de los 3 stacks
En Parte 1 montaron Loki. En Parte 2 montaron EFK. Hoy probablemente la pregunta es: “¿cuál elijo?”. Esta parte responde con una pregunta mejor: “¿por qué tengo que elegir uno?”. OpenTelemetry permite usar los DOS al mismo tiempo, y migrar entre backends sin tocar el código de la app.
🤝 Partes 3 y 4 se entregan el mismo día (09/05). Conviene trabajarlas en paralelo: a medida que avanzan los hits de la Parte 3, vayan tomando las mediciones de la Parte 4 (el
topdel Pod, las latencias de las queries equivalentes). Cuando llegan al Hit #5 de Parte 3 ya tienen los datos para escribir el ADR magisterial de Parte 4. Un solo push final, dos entregables.
Pre-requisitos: Partes 1 y 2 entregadas. Loki y EFK siguen corriendo (en sus namespaces
observabilityyelastic). En esta parte vamos a desconectar Promtail/Alloy y Fluent Bit, y centralizar todo en OTel Collector que va a hacer fan-out hacia ambos backends.
Pre-requisitos:
- TP 2 · Parte 1 entregado y aprobado: stack Loki +
Promtail/Alloy + Grafana operativo en namespace
observability. Datasource Loki en Grafana respondiendo. Sin esto no hay backend #1 al cual exportar. - TP 2 · Parte 2 entregado y aprobado: stack EFK
(Elasticsearch + Fluent Bit + Kibana) operativo en namespace
elastic. Indices del scraper visibles en Kibana. Sin esto no hay backend #2 al cual exportar. - Cluster k3s/k3d operativo con al menos 8 GB de RAM libre y 10 GB de disco — el OTel Collector en sí es liviano (~150 MB), pero suma sobre los stacks de las Partes 1 y 2 que ya están corriendo en paralelo.
- Familiaridad con
kubectl,helm, manifests + experiencia previa con Custom Resource Definitions (CRDs) del TP 1 · Parte 2. Si nunca aplicaron un CRD, repasar el concepto antes de arrancar este TP. - Helm 3 instalado (
helm version≥ 3.16). Heredado de las partes anteriores, no debería faltar. - Imagen Docker del scraper ya publicada (cualquier registry: GHCR, Docker Hub, ECR público) — porque en el Hit #5 vamos a publicar una nueva versión instrumentada con el SDK de OTel.
Requisitos, consideraciones y formato de entrega
Aplican los mismos requisitos generales del TP 1 y TP 2 · Partes 1 y 2 (repo público, README por hit, video, integración con IA documentada, sin secrets commiteados) más los siguientes específicos de esta Parte 3.
Infra base obligatoria — bloqueante
🚧 Sin esto la entrega no se puede evaluar. La cátedra corre tu
helm install+kubectl apply+ abre Grafana y Kibana en el browser para verificar que el mismo log entra a los dos backends. Si el fan-out no funciona, se evalúa parcial pero el grueso del TP es justamente eso → el peso del Hit #3 está al 25 % por una razón. No suma puntos en la rúbrica porque es condición necesaria.
Carpeta
otel/en el repo con todo lo declarativo y reproducible:otel/ ├── README.md ← cómo levantar el collector + cómo verificar fan-out ├── helm/ │ └── otel-operator-values.yaml ← values del chart open-telemetry/opentelemetry-operator ├── manifests/ │ ├── namespace.yaml ← namespace `otel` │ ├── collector-agent.yaml ← OpenTelemetryCollector CRD (mode: daemonset) │ ├── rbac.yaml ← ServiceAccount + ClusterRole (k8sattributes processor) │ └── scraper-otlp-config.yaml ← ConfigMap con OTEL_EXPORTER_OTLP_ENDPOINT para el scraper ├── scraper-instrumentation/ │ ├── otel_setup.py ← módulo Python con TracerProvider/LoggerProvider │ └── requirements-otel.txt ← deps OTel pinneadas ├── screenshots/ │ ├── hit3-fanout-loki.png ← mismo log_id en Loki │ ├── hit3-fanout-elastic.png ← mismo log_id en Kibana │ ├── hit5-otlp-trace.png ← traza del scraper en Grafana Tempo (bonus) │ └── hit4-old-agents-down.png ← Promtail + Fluent Bit con replicas=0 + logs siguen llegando └── install.sh ← script idempotente con todos los pasosScript
otel/install.shreproducible que despliega el operator + el collector como CRD desde cero, contra los stacks de Partes 1 y 2 ya corriendo. La cátedra lo ejecuta tal cual:cd otel && ./install.sh # Output esperado al final: # ✓ OpenTelemetry Operator running (kubectl get pod -n otel-operator-system) # ✓ OpenTelemetryCollector CRD aplicado (kubectl get otelcol -n otel) # ✓ Collector DaemonSet running con 1 pod por nodo # ✓ Pipeline activo: filelog → batch → k8sattributes → [loki, elasticsearch] # ✓ Promtail (namespace observability) escalado a 0 # ✓ Fluent Bit (namespace elastic) escalado a 0 # → Verificá fan-out en Grafana http://<node-ip>:30000 y Kibana http://<node-ip>:30001Helm chart pinneado a versión específica (no
latesten ninguna release). Para la entrega 2026:Componente Chart Versión chart App version Repo OpenTelemetry Operator open-telemetry/opentelemetry-operator0.74.xOperator 0.110.x https://open-telemetry.github.io/opentelemetry-helm-charts OpenTelemetry Collector (image) otel/opentelemetry-collector-contrib0.110.x— https://hub.docker.com/r/otel/opentelemetry-collector-contrib OpenTelemetry Python SDK opentelemetry-sdk1.30.x— PyPI OpenTelemetry Python OTLP exporter opentelemetry-exporter-otlp1.30.x— PyPI ⚠️ Usar
opentelemetry-collector-contrib, noopentelemetry-collector. La distribucióncoredel collector no incluye los exporters de Loki ni Elasticsearch — esos viven solo encontrib. Si bajan el core van a tener errores tipounknown exporter "loki"cuando apliquen el CRD. La diferencia de tamaño (~80 MB extra) es irrelevante en este TP.⚠️ No instalen el collector standalone sin el operator. El chart
open-telemetry/opentelemetry-collectordespliega un Deployment/DaemonSet directo y elOpenTelemetryCollectorno queda como CRD declarativo. Para cumplir con el Hit #1 tienen que ir por el operator.Secrets para conectar a Loki y Elasticsearch via
kubectl create secret. El password de Elastic ya existe del TP 2 · Parte 2 (elastic-credentialsen namespaceelastic). El collector necesita poder leerlo — elinstall.shdebe copiar el secret al namespaceotel:kubectl get secret elastic-credentials -n elastic -o yaml \ | sed 's/namespace: elastic/namespace: otel/' \ | kubectl apply -f -gitleaksen pre-commit y CI: heredado de las Partes 1 y 2. Si alguien commitea un endpoint con basic-auth embebido (http://elastic:passw0rd@elastic-master:9200), el push debe fallar.
Otros requisitos
Mínimo 1 ADR obligatorio en
docs/adr/(continúan la numeración del TP 2 · Partes 1 y 2):0010-instrumentacion-vendor-neutral.mdjustificando por qué OpenTelemetry y no seguir con Promtail + Fluent Bit corriendo en paralelo. Mismo formato Michael Nygard. Tiene que mencionar al menos: lock-in con Grafana Labs y Elastic, el costo operativo de mantener dos agentes en cada nodo, el compromiso de la industria con OTLP (los 4 grandes proveedores SaaS — Datadog, New Relic, Dynatrace, Splunk — soportan OTLP nativo desde 2024-2025), y por qué la inversión en re-instrumentar el scraper paga a mediano plazo. Ojo: en la Parte 4 este ADR se va a comparar con el0007(logging) y el0009(EFK) en el ADR comparativo final.Bonus opcional — 1 ADR adicional (
0011-traces-vs-solo-logs.md) si hacen el Hit #6. Tiene que justificar por qué sumar traces (con el overhead que tienen) además de logs en un scraper relativamente simple, o explicar por qué decidieron no hacerlo.NO desinstalar Loki ni EFK. La idea de esta Parte 3 es que los dos backends sigan corriendo en paralelo y reciban los mismos logs. Los DaemonSets viejos (Promtail / Fluent Bit) sí se escalan a 0 (Hit #4) pero los stacks de storage / visualización se mantienen.
Resources limitados — es un cluster local k3s con 3 stacks corriendo en paralelo. Cualquier values.yaml que pida más de lo siguiente se considera mal calibrado:
Componente Requests Limits Storage OpenTelemetry Operator 64 Mi RAM, 50m CPU 128 Mi RAM, 200m CPU — OTel Collector (DaemonSet) 128 Mi RAM, 100m CPU 256 Mi RAM, 300m CPU — (lee del host) Jaeger (bonus, traces) 256 Mi RAM, 100m CPU 512 Mi RAM, 500m CPU 1 Gi PVC local-pathMantener las buenas prácticas del TP 1 y TP 2: explicit waits, selectores en módulo aparte, logging estructurado JSON, dashboards as-code. No regresionar. El Hit #5 reemplaza el
logging_setup.pydel TP 2 · Parte 1 — pero los call-sites conextra={...}siguen siendo iguales (esa es justamente la gracia: el código de negocio no cambia).
Contenidos del programa relacionados
- Observabilidad: las 3 señales (logs, métricas, traces) y por qué OTel es el primer estándar que las trata como ciudadanos de primera clase en un mismo SDK.
- Vendor neutrality: el costo del lock-in en agentes de observabilidad y por qué CNCF graduated significa “este proyecto no se va a morir”.
- Protocolo OTLP: gRPC + protobuf para señales de telemetría, comparado con Loki push API, Bulk API de Elasticsearch, syslog y fluentd protocol.
- Arquitectura de OTel Collector: receivers, processors, exporters, connectors, pipelines.
- Patrón Agent vs Gateway en OTel: por qué casi todo deploy real usa los dos.
- Instrumentación: SDK manual vs auto-instrumentation (Java agent,
Python
opentelemetry-instrument). - Fan-out / multi-backend:
service.pipelines.logs.exporters: [loki, elasticsearch]— el cambio de 5 líneas que hace migrar de stack trivial. - CRDs y operator pattern para deploys declarativos en k8s.
Conceptos clave de OpenTelemetry
📚 Esta sección es obligatoria de leer antes de tocar nada. OTel tiene una superficie de API más grande que Loki o EFK — si saltean los conceptos van a copiar YAMLs sin entender qué hacen y cuando algo falle (y va a fallar) no van a saber por dónde empezar. Léanla. Son ~15 min.
Las 3 señales
OTel define 3 tipos de telemetría que cualquier sistema observable produce:
| Señal | Qué responde | Ejemplo en el scraper |
|---|---|---|
| Logs | “¿Qué pasó en este momento puntual?” | INFO Scrape iniciado producto=iphone |
| Metrics | “¿Cuál es el valor agregado de algo en el tiempo?” | scraper_pages_total{producto="iphone"} 142 |
| Traces | “¿Cómo fluye una operación a través del sistema?” | Span “fetch_html” (1.2 s) → “parse_results” (84 ms) → “write_postgres” (12 ms) |
🎯 En este TP cubrimos solo logs (obligatorio) + traces (bonus Hit #6). Las métricas las dejamos para otra materia — el scraper es batch, no servidor, y métricas tipo “QPS” no aplican. Si quieren métricas de igual modo, se sumarían como un receiver más en el mismo collector — esa es justamente la elegancia de OTel.
Arquitectura del flujo OTLP
┌─────────────────┐ OTLP/gRPC ┌──────────────────┐
│ App + OTel SDK │ ────────────────────▶ │ OTel Collector │
│ (scraper.py) │ :4317 │ (DaemonSet) │
└─────────────────┘ └──────────────────┘
│
┌────────┼────────┐
▼ ▼ ▼
[Loki] [ElasticSearch] [Tempo/Jaeger]
(logs) (logs) (traces)
3 capas, conceptualmente independientes:
- SDK — biblioteca dentro de la app
(
opentelemetry-sdkpara Python). Genera spans, métricas y log records, los serializa a OTLP y los manda al collector. La app no sabe a qué backend van — eso es el punto. - Collector — proceso intermedio. Recibe OTLP (o casi
cualquier otro formato — receivers
filelog,prometheus,jaeger,zipkin,kafka, etc.), aplica processors (batch, filter, transform, k8sattributes para enriquecer con metadata k8s), y exporta a uno o más backends. - Backends — Loki, Elasticsearch, Prometheus, Tempo, Jaeger, Datadog, New Relic, lo que sea. Los exporters son módulos del collector — agregarle uno nuevo es agregar 5 líneas al YAML.
Por qué OTLP
OTLP (OpenTelemetry Protocol) es gRPC + protobuf (también soporta HTTP/protobuf y HTTP/JSON, pero gRPC es lo recomendado para alto volumen). Su valor no es técnico — los protocolos anteriores también funcionaban — es político: es el primer protocolo de telemetría que los 4 grandes proveedores SaaS aceptaron implementar nativamente.
| Protocolo | Año origen | Vendor-neutral | Multi-señal | Adopción 2026 |
|---|---|---|---|---|
| Syslog (RFC 5424) | 1980s | Sí | Solo logs | Universal pero limitado |
| Fluentd Forward Protocol | 2011 | Parcial (Fluent Project) | Solo logs | Decreciente — Fluent Bit migra a OTLP |
| InfluxDB Line Protocol | 2014 | No (InfluxData) | Solo metrics | Específico de Influx |
| Loki HTTP push API | 2018 | No (Grafana Labs) | Solo logs | Decreciente — Loki ya soporta OTLP nativo desde 2.9 |
| Elasticsearch Bulk API | ~2010 | No (Elastic) | Solo logs | Estándar dentro del ecosistema Elastic |
| OTLP | 2019 | Sí (CNCF) | Logs + Metrics + Traces | Estándar de facto desde 2024 |
El Hit #3 de este TP es la prueba de concepto: el scraper habla un solo protocolo (OTLP), y el collector lo traduce a Loki HTTP push API y a Elasticsearch Bulk API en simultáneo. Si mañana querés sumar Datadog, agregás un exporter más — el scraper no se entera.
Roles del Collector: Agent vs Gateway
| Rol | Deploy | Función | Cuándo |
|---|---|---|---|
| Agent | DaemonSet (1 pod por nodo) | Recolecta telemetría local del nodo (filelog, hostmetrics) y de apps locales | Casi siempre — es la base |
| Gateway | Deployment central (replicas: 2-5) | Recibe de los agents, aplica processors costosos (sampling, redaction, enriquecimiento global), exporta a backends | Producción mediana/grande, cuando los agents son demasiados clientes para el backend o se necesita egress controlado |
En este TP usamos solo el rol Agent. Un cluster local con 1 nodo no se beneficia de un gateway. En producción real, lo típico es: agent en cada nodo → gateway central → backends. El TP 2 · Parte 4 lo discute como evolución posible.
Components del Collector
El YAML del OpenTelemetryCollector tiene 4 secciones
principales:
| Sección | Qué hace | Ejemplos |
|---|---|---|
| receivers | Cómo entran los datos | otlp (gRPC :4317), filelog (lee archivos),
hostmetrics, kafka, prometheus
(scraping) |
| processors | Qué se hace antes de exportar | batch (agrupa N items o T tiempo),
k8sattributes (enriquece con metadata k8s),
attributes (rename/insert/delete fields),
filter (drop), transform (OTTL — DSL) |
| exporters | A dónde se mandan los datos | loki, elasticsearch, otlp (a
otro collector), prometheus, jaeger,
kafka, debug (stdout) |
| service.pipelines | Cómo se conectan | logs: receivers=[filelog,otlp] processors=[batch,k8sattributes] exporters=[loki,elasticsearch] |
Hay un quinto tipo, connectors, que son receiver + exporter al mismo tiempo — sirven para hacer pipelines complejos tipo “de spans, generá métricas”. No los usamos en este TP.
CNCF graduated, multi-vendor
OpenTelemetry es CNCF graduated desde noviembre 2023 — el segundo nivel de madurez de la CNCF (junto con Kubernetes, Prometheus, Envoy, etc.), reservado para proyectos que tienen gobernanza estable, múltiples mantenedores corporativos y adopción industrial real. En la práctica esto significa:
- No se va a morir. Tiene committers de Microsoft, Google, AWS, Splunk, Datadog, Honeycomb, Elastic, Grafana Labs y muchos más. Ningún proveedor solo puede matar el proyecto.
- Los grandes lo soportan nativamente. Datadog, New Relic, Dynatrace y Splunk (los 4 SaaS dominantes en observabilidad enterprise) aceptan OTLP directo desde sus endpoints de ingesta — no hay agente propio obligatorio. Eso es lo que rompió el lock-in histórico de “instalá el agente de Datadog en cada host”.
- Los proyectos OSS también. Loki ≥ 2.9, Elasticsearch ≥ 8.10, Prometheus ≥ 2.50 (vía remote-write OTLP), Jaeger ≥ 1.51, Tempo desde 2022 — todos aceptan OTLP nativo en 2026.
OpenTelemetry no es perfecto — honestidad técnica
Antes de venderlo como bala de plata, conviene saber qué duele:
- El SDK de Python es el menos maduro de los grandes (Java/Go > .NET > Python > Ruby > JS). Logs API recién dejó de ser “experimental” en 2024. Si tienen problemas con auto-instrumentation de algunas libs, no se sorprendan.
- Traces tienen overhead notable. Cada span agrega ~30-100 µs en la app, más el costo de exportar OTLP. En el scraper (que vive haciendo I/O de cientos de ms) es ruido — pero en un servicio sub-ms (caché en memoria, stream processor) es 5-10 % de degradación.
- El YAML del collector es propenso a errores
silenciosos. Si escribís un nombre de processor mal, el
collector arranca pero ignora el processor y nadie te avisa. Mirar
kubectl logsdel collector siempre que algo no llega. - Los receivers
filelog(que usamos en el Hit #2) compiten con los archivos del host con rotation. Si el log rota mientras el receiver lee, podés perder líneas en el momento del rotate. La doc oficial recomienda usar el receiverfilelogconstart_at: endy aceptar que en boot perdés histórico. - Para métricas de alto volumen, el collector single-instance
es el cuello de botella. Para algo serio se hace HA con un
gateway con
loadbalancingexporter y receivers replicados — fuera del scope de este TP.
Dicho esto: la alternativa (mantener Promtail + Fluent Bit + algún agente de métricas + algún agente de traces, todos con su propio formato y operación) es peor. OTel gana por consolidación, no por ser perfecto en cada punto.
Práctica
En la Parte 1 y la Parte 2 montaron dos stacks completos de logging
centralizado, cada uno con su propio agente de recolección (Promtail /
Fluent Bit), su propio backend (Loki / Elasticsearch) y su propia UI
(Grafana / Kibana). Ambos funcionan. Y en este momento están
duplicando el trabajo: cada nodo del cluster corre
dos DaemonSets que leen los mismos
archivos /var/log/pods/* y los mandan a
dos backends distintos, cada uno con su protocolo, su
API y su evolución independiente.
Eso es el problema del 2026 que OTel resuelve. La pregunta no es “¿Loki o Elastic?” — es “¿por qué tengo dos agentes haciendo lo mismo, hablando dos protocolos distintos, y cada uno me ata a un proveedor distinto?”.
La respuesta de la industria desde 2019 — y especialmente desde que CNCF lo graduó en 2023 — es OpenTelemetry: un protocolo común (OTLP), un collector central que sabe hablar con todos los backends, y SDKs que no atan al código de la app a ningún proveedor. El día que mañana decidan cambiar Loki por Datadog, el scraper no cambia una línea.
El Hit clave de esta Parte 3 es el #3: van a configurar el OTel Collector para que el mismo flujo de logs salga simultáneamente a Loki Y a Elasticsearch. Es la prueba de concepto de que la abstracción funciona. A partir de ahí, cambiar de backend deja de ser una migración traumática de meses y pasa a ser editar 5 líneas de YAML.
Hit #1 — Deploy del OpenTelemetry Operator
El OTel Collector se puede deployar de muchas formas (chart Helm
directo, manifest a mano, sidecar). En 2026 el camino estándar
k8s-native es a través del OpenTelemetry Operator: un
operador que define el CRD OpenTelemetryCollector y se
encarga de levantar el Deployment/DaemonSet/Sidecar correspondiente
cuando vos aplicás un manifest.
Por qué CRD en lugar de Deployment a mano:
- Declarativo. Un solo YAML con todo el pipeline
(receivers + processors + exporters + service). Si querés cambiarlo,
kubectl applyy el operator hace el rolling update. - Validación. El CRD valida la sintaxis del config del collector antes de aplicarlo. Si te equivocás en un nombre de exporter, te lo dice — no se entera el collector después de bootear.
- Modos múltiples con un solo recurso. Cambiar
mode: daemonsetpormode: deploymentreusa el mismo manifest, sin tocar los manifests subyacentes. - Auto-instrumentation viene gratis. El mismo
operator incluye el CRD
Instrumentationque inyecta auto-instrumentation a pods con un annotation. Lo usaríamos en el bonus si quisieran instrumentar el scraper sin tocar el código (no es lo que pide el Hit #5, pero conviene saber que existe).
1.1 — Namespace y repo Helm
kubectl create namespace otel
kubectl create namespace otel-operator-system
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update1.2 — Cert-manager (dependencia del operator)
El OTel Operator usa webhooks admission, y los webhooks requieren TLS. La forma estándar de proveer ese TLS en k8s es con cert-manager, que probablemente ya tengan instalado del TP 2 · Parte 2 (EFK requiere cert-manager para el ECK operator). Si no lo tienen, instalarlo:
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.16.1 \
--set installCRDs=trueVerificar que arranque:
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=120s1.3 — OpenTelemetry Operator
otel/helm/otel-operator-values.yaml:
manager:
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi }
# El operator necesita ver pods en todos los namespaces (clusterwide)
watchNamespace: ""
admissionWebhooks:
certManager:
enabled: true # usa cert-manager para los webhooks (recomendado en prod)
# Instalá los CRDs (OpenTelemetryCollector + Instrumentation)
crds:
create: truehelm install otel-operator open-telemetry/opentelemetry-operator \
--version 0.74.0 \
--namespace otel-operator-system \
--values otel/helm/otel-operator-values.yaml \
--wait --timeout 5mVerificar:
kubectl -n otel-operator-system rollout status deploy/otel-operator-controller-manager --timeout=120s
kubectl get crds | grep opentelemetry
# Esperado:
# instrumentations.opentelemetry.io
# opentelemetrycollectors.opentelemetry.io
# opampbridges.opentelemetry.ioOutput esperado del Hit #1
$ kubectl -n otel-operator-system get pods
NAME READY STATUS RESTARTS AGE
otel-operator-controller-manager-xxxxxxxxx-xxxxx 2/2 Running 0 2m
$ kubectl get crds | grep opentelemetry | wc -l
3
Los 2 contenedores en el pod del manager son el manager
propiamente dicho y un kube-rbac-proxy que hace authz para
el endpoint de métricas — esto es estándar de operadores construidos con
kubebuilder.
Hit #2 — OpenTelemetryCollector en modo Agent (DaemonSet) leyendo logs
Ahora desplegamos el Collector como CRD
OpenTelemetryCollector en modo DaemonSet.
Este es nuestro rol “Agent”: un pod por nodo, leyendo
/var/log/pods/ directo del filesystem del host (mismo
paradigma que Promtail y Fluent Bit en las Partes 1 y 2 — solo que ahora
con OTel).
Pipeline objetivo del Hit #2 (lo que vamos a poner en el CRD):
[receivers] [processors] [exporters]
filelog ────────────▶ batch ────▶ k8sattributes ────▶ debug (stdout solo en este hit)
↓
attributes
(rename loki/elastic-friendly fields)
En este Hit todavía NO exportamos a Loki/Elastic —
usamos solo el exporter debug para confirmar que el
pipeline procesa logs correctamente. El fan-out llega en el Hit #3.
2.1 — RBAC: ServiceAccount + ClusterRole
El processor k8sattributes necesita listar pods,
namespaces y nodes para enriquecer los logs con metadata k8s. Eso
requiere RBAC explícito:
otel/manifests/rbac.yaml:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: otel-collector
namespace: otel
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: otel-collector
rules:
- apiGroups: [""]
resources: ["pods", "namespaces", "nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["replicasets", "daemonsets", "statefulsets", "deployments"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
- apiGroups: ["extensions"]
resources: ["replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: otel-collector
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: otel-collector
subjects:
- kind: ServiceAccount
name: otel-collector
namespace: otelkubectl apply -f otel/manifests/rbac.yaml2.2 — OpenTelemetryCollector CRD (modo DaemonSet)
otel/manifests/collector-agent.yaml:
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: agent
namespace: otel
spec:
mode: daemonset
image: otel/opentelemetry-collector-contrib:0.110.0
serviceAccount: otel-collector
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 300m, memory: 256Mi }
# Tolera taints del control plane (k3s single-node)
tolerations:
- effect: NoSchedule
operator: Exists
# Mountamos /var/log y /var/lib/docker/containers para que filelog pueda leer
volumeMounts:
- name: varlog
mountPath: /var/log
readOnly: true
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath: { path: /var/log }
- name: varlibdockercontainers
hostPath: { path: /var/lib/docker/containers }
config:
receivers:
filelog:
include:
- /var/log/pods/ml-scraper_*/*/*.log
# Empieza al final del archivo en cada boot — evita re-leer histórico
start_at: end
include_file_path: true
include_file_name: false
operators:
# Parsea el formato CRI logs (timestamp + stream + flag + log)
- type: container
id: container-parser
# OTLP receiver para el Hit #5 (cuando el scraper exporte directo via SDK)
otlp:
protocols:
grpc: { endpoint: 0.0.0.0:4317 }
http: { endpoint: 0.0.0.0:4318 }
processors:
batch:
timeout: 10s
send_batch_size: 1024
# Enriquece con metadata k8s (namespace, pod, container, labels)
k8sattributes:
auth_type: serviceAccount
passthrough: false
extract:
metadata:
- k8s.namespace.name
- k8s.pod.name
- k8s.pod.uid
- k8s.deployment.name
- k8s.statefulset.name
- k8s.daemonset.name
- k8s.cronjob.name
- k8s.job.name
- k8s.node.name
labels:
- tag_name: app
key: app
from: pod
- tag_name: component
key: app.kubernetes.io/component
from: pod
pod_association:
- sources:
- from: resource_attribute
name: k8s.pod.uid
- sources:
- from: connection
# Renombrar campos para que sean idiomáticos en cada backend
attributes:
actions:
# Loki prefiere "service" como label primario
- key: service
from_attribute: app
action: insert
exporters:
# En este Hit solo debug. Hit #3 agrega loki + elasticsearch.
debug:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
service:
pipelines:
logs:
receivers: [filelog, otlp]
processors: [k8sattributes, attributes, batch]
exporters: [debug]
telemetry:
logs:
level: infokubectl apply -f otel/manifests/collector-agent.yamlVerificar:
kubectl -n otel get otelcol
# NAME MODE VERSION READY AGE
# agent daemonset 0.110.0 1/1 2m
kubectl -n otel get ds/agent-collector
# DESIRED CURRENT READY ... AGE
# 1 1 1 3m2.3 — Disparar tráfico y validar pipeline
# Disparar un Job manual del scraper (mismo del TP 1 · P2 Hit #7)
kubectl -n ml-scraper create job --from=cronjob/scraper-hourly scraper-otel-test-1
kubectl -n ml-scraper wait --for=condition=complete job/scraper-otel-test-1 --timeout=600s
# Mirar los logs del collector
kubectl -n otel logs ds/agent-collector | grep -A 5 "ResourceLog"Output esperado del Hit #2
En kubectl logs ds/agent-collector deben ver bloques
tipo:
ResourceLog #0
Resource SchemaURL:
Resource attributes:
-> k8s.namespace.name: Str(ml-scraper)
-> k8s.pod.name: Str(scraper-otel-test-1-xxxxx)
-> k8s.cronjob.name: Str(scraper-hourly)
-> service: Str(scraper)
-> k8s.node.name: Str(k3d-cluster-server-0)
ScopeLogs #0
LogRecord #0
ObservedTimestamp: 2026-05-22 03:14:22.123 +0000 UTC
Body: Str({"timestamp":"...","level":"INFO","logger":"extractors","message":"Scrape iniciado","producto":"iphone"})
Si ven los bloques con el atributo
k8s.namespace.name y compañía, el pipeline
filelog → k8sattributes → debug está funcionando. Si ven el
Body pero sin los atributos k8s, el
processor k8sattributes no está enriqueciendo — revisar el
RBAC del 2.1 y los logs del collector buscando errores tipo
failed to list pods.
Capturen un screenshot del output del debug exporter
y commitéenlo en
otel/screenshots/hit2-debug-output.png.
Hit #3 — Fan-out simultáneo a Loki + Elasticsearch (el hit clave)
Este es el hit de la Parte 3. Demostrar que el mismo log entra a los dos backends sin código duplicado, sin agentes duplicados, y cambiar de uno a los dos es agregar líneas a un YAML.
Modifiquen el OpenTelemetryCollector del Hit #2
para:
- Sumar dos exporters:
lokiyelasticsearch. - Listar ambos en el pipeline de logs:
exporters: [loki, elasticsearch](sacardebugo dejarlo, da igual). - Inyectar un campo
log_idúnico por log line para poder verificar visualmente que el mismo evento apareció en los dos backends.
3.1 —
Copiar las credenciales de Elastic al namespace otel
Heredado del TP 2 · Parte 2:
kubectl get secret elastic-credentials -n elastic -o yaml \
| sed 's/namespace: elastic/namespace: otel/' \
| grep -v 'creationTimestamp\|resourceVersion\|uid\|selfLink' \
| kubectl apply -f -Verificar:
kubectl -n otel get secret elastic-credentials -o jsonpath='{.data.password}' | base64 -d
# Tiene que devolver el password de elastic, no estar vacío3.2 — Modificar el CRD del collector
Agregar a otel/manifests/collector-agent.yaml en la
sección exporters (al lado de debug):
exporters:
debug:
verbosity: basic # bajamos verbosity ahora que el pipeline funciona
# === Backend #1: Loki ===
otlphttp/loki:
endpoint: http://loki.observability.svc.cluster.local:3100/otlp
# Loki ≥ 3.0 acepta OTLP nativo en el endpoint /otlp
# (forma "moderna", reemplaza al exporter "loki" legacy)
# === Backend #2: Elasticsearch ===
elasticsearch:
endpoints:
- https://elasticsearch-master.elastic.svc.cluster.local:9200
user: elastic
password: ${env:ELASTIC_PASSWORD}
tls:
insecure_skip_verify: true # cert self-signed del ECK operator
# Index naming siguiendo la convención del TP 2 · Parte 2
logs_index: scraper-logs
# Mappings dinámicos: que el campo "level" sea keyword, no text
mapping:
mode: ecsY agregar la env var ELASTIC_PASSWORD al spec del
collector (al nivel de spec, no dentro de
config):
spec:
mode: daemonset
image: otel/opentelemetry-collector-contrib:0.110.0
serviceAccount: otel-collector
env:
- name: ELASTIC_PASSWORD
valueFrom:
secretKeyRef:
name: elastic-credentials
key: password
# ... resto igualY en service.pipelines.logs.exporters reemplazar
[debug] por:
service:
pipelines:
logs:
receivers: [filelog, otlp]
processors: [k8sattributes, attributes, batch]
exporters: [otlphttp/loki, elasticsearch]💡 Por qué
otlphttp/lokiy no el exporterlokilegacy: hasta 2024 OTel tenía un exporter dedicadolokique hablaba el push API HTTP de Loki. Funcionaba pero perdía estructura (Loki guardaba todo como texto plano y los atributos como labels — explosión de cardinality). Desde Loki 3.0 (mid-2024) Loki acepta OTLP nativo en/otlp, preservando la estructura completa de OTel. El exporterlokiestá deprecado en 0.110+ y se va a eliminar en 0.120+. Usenotlphttp/loki.
3.3 — Inyectar un
log_id único para verificar
En el processor attributes, sumar una acción que genere
un UUID por log:
attributes:
actions:
- key: service
from_attribute: app
action: insert
- key: log_id
value: ""
action: insert
# Loki/Elastic se completa con un UUID (no se puede en attributes plano,
# se hace via processor "transform" con OTTL):Como attributes no genera UUIDs, en lugar de eso usen el
processor transform (OTTL — la DSL de transforms del
collector):
transform:
log_statements:
- context: log
statements:
- 'set(attributes["log_id"], UUID()) where attributes["log_id"] == nil'Y agreguen transform al pipeline:
logs:
receivers: [filelog, otlp]
processors: [k8sattributes, attributes, transform, batch]
exporters: [otlphttp/loki, elasticsearch]Apliquen:
kubectl apply -f otel/manifests/collector-agent.yaml
kubectl -n otel rollout status ds/agent-collector --timeout=120s3.4 — Disparar tráfico y verificar en los dos backends
kubectl -n ml-scraper create job --from=cronjob/scraper-hourly scraper-fanout-1
kubectl -n ml-scraper wait --for=condition=complete job/scraper-fanout-1 --timeout=600sDespués, en Grafana → Explore → Loki, query:
{service="scraper", k8s_namespace_name="ml-scraper"} | json | line_format "{{.log_id}} {{.message}}"
Copien un log_id cualquiera del output (ejemplo:
aa1b2c3d-...) y vayan a Kibana → Discover
→ index scraper-logs-* → search:
log_id : "aa1b2c3d-..."
Tiene que aparecer el mismo evento en los dos backends. Si solo aparece en uno:
- Si solo aparece en Loki: el exporter de Elasticsearch está fallando.
kubectl logs ds/agent-collector | grep -i elastic— buscar errores tipotls verify failedounauthorized. Verificar el secret. - Si solo aparece en Elastic: el endpoint OTLP de Loki está mal.
Verificar que Loki esté en versión ≥ 3.0 (en el TP 2 · P1 pinneamos
lokichart 6.16+ → Loki 3.x → ok).
Output esperado del Hit #3
- Mismo
log_idvisible en Grafana y en Kibana. kubectl logs ds/agent-collectorsin errores tipofailed to pushoconnection refused.- En el Service de Loki,
loki_distributor_lines_received_total(métrica interna) creciendo, y en Elastic, el indexscraper-logs-*con_countcreciendo.
Capturen 2 screenshots side-by-side:
otel/screenshots/hit3-fanout-loki.png— Grafana mostrando el log conlog_id.otel/screenshots/hit3-fanout-elastic.png— Kibana mostrando el mismolog_id.
Estos dos screenshots son la entrega del Hit #3. Sin ellos, este hit es 0.
Hit #4 — Reemplazar Promtail/Alloy + Fluent Bit por solo OTel Collector
Si el Hit #3 funcionó, los dos agentes viejos están
redundantes — el OTel Collector ya está leyendo los
mismos archivos /var/log/pods/* y mandándolos a los dos
backends. Es el momento de apagar los agentes viejos y demostrar que el
flujo sigue funcionando solo con OTel.
4.1 — Escalar Promtail a 0 (TP 2 · Parte 1)
# Si usaron Promtail (default del TP 2 · P1):
kubectl -n observability scale ds/promtail --replicas=0
# Si optaron por Alloy:
# kubectl -n observability scale ds/alloy --replicas=0
kubectl -n observability get ds
# DESIRED CURRENT READY UP-TO-DATE AVAILABLE ...
# 0 0 0 0 0💡 Por qué
scale --replicas=0y nohelm uninstall. Si la cátedra detecta un problema con OTel y necesitan rollback, escalar a 1 vuelve a tener Promtail/Fluent Bit corriendo en 30 segundos. Desinstalar el chart es destructivo y reinstalarlo lleva varios minutos. En producción real, el patrón es siempre el mismo: dejá el sistema viejo “frío” durante el período de bake del nuevo (típico: 1-2 semanas), y solo después desinstalá.
4.2 — Escalar Fluent Bit a 0 (TP 2 · Parte 2)
kubectl -n elastic scale ds/fluent-bit --replicas=0
kubectl -n elastic get ds/fluent-bit
# DESIRED == 0, READY == 04.3 — Verificar que el flujo sigue funcionando
kubectl -n ml-scraper create job --from=cronjob/scraper-hourly scraper-otel-only-1
kubectl -n ml-scraper wait --for=condition=complete job/scraper-otel-only-1 --timeout=600sEn Grafana → Explore → Loki:
{service="scraper", k8s_namespace_name="ml-scraper", k8s_job_name="scraper-otel-only-1"}
Tienen que aparecer logs. Y en Kibana, mismo job:
k8s.job.name : "scraper-otel-only-1"
Output esperado del Hit #4
- Promtail (o Alloy) y Fluent Bit con
replicas=0. kubectl get pods -n observabilityykubectl get pods -n elasticmuestran solo Loki/Elastic/Kibana/Grafana — sin DaemonSets de logging.- OTel Collector en namespace
otelactivo (un pod por nodo). - Los logs del último Job aparecen en los dos backends.
Capturen un screenshot mostrando los 2 DaemonSets viejos en 0
+ el Collector OTel activo + el último log en los 2 backends, y
commitéenlo en
otel/screenshots/hit4-old-agents-down.png.
🎯 Esto es el momento “wow” del TP. Los alumnos típicamente reaccionan acá — porque acaban de eliminar 2 agentes (50 % menos pods, 50 % menos config a mantener) sin perder nada. Y todavía no tocaron código del scraper. Ese es el siguiente hit.
Hit #5 — Instrumentar el scraper con el SDK de OpenTelemetry
Hasta ahora el OTel Collector lee del filesystem
(receiver filelog) — exactamente como hacían Promtail y
Fluent Bit. Eso funciona pero tiene 2 limitaciones:
- Pasa por stdout → CRI → archivo. Cada salto suma latencia y oportunidades de pérdida (rotation, etc.).
- No hay correlación con traces. Si querés
correlacionar un log con la traza que lo originó (Hit #6 bonus),
necesitás que la app emita los dos con un
trace_idcompartido — eso solo se logra con SDK.
La forma “correcta” en OTel es: la app habla OTLP directo al Collector via gRPC, sin pasar por archivos. Vamos a hacer eso.
5.1 — Instalar las dependencias OTel en el scraper
scraper-instrumentation/requirements-otel.txt:
opentelemetry-api==1.30.0
opentelemetry-sdk==1.30.0
opentelemetry-exporter-otlp-proto-grpc==1.30.0
Sumar a requirements.txt del scraper (heredado del TP 1)
las 3 líneas anteriores.
5.2 — Reemplazar
logging_setup.py
El módulo del TP 2 · Parte 1 emitía JSON a stdout via
python-json-logger. Lo reemplazamos por uno que use el
LoggingHandler de OTel — emite log records via OTLP/gRPC al
collector, sin pasar por archivos.
scraper-instrumentation/otel_setup.py:
"""
Setup de OpenTelemetry SDK: TracerProvider + LoggerProvider con OTLP exporter.
Reemplaza logging_setup.py del TP 2 · Parte 1.
"""
import logging
import os
import socket
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import (
OTLPLogExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
def setup_otel(service_name: str = "scraper") -> None:
"""
Configura logging + tracing OTel.
Lee OTEL_EXPORTER_OTLP_ENDPOINT (default: http://localhost:4317).
Si la app está en un Pod, este env var se inyecta via ConfigMap apuntando
al collector DaemonSet en el mismo nodo (ver scraper-otlp-config.yaml).
"""
endpoint = os.environ.get(
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"
)
# Resource: metadata adjunta a todas las señales
resource = Resource.create(
{
"service.name": service_name,
"service.version": os.environ.get("APP_VERSION", "dev"),
"host.name": socket.gethostname(),
"deployment.environment": os.environ.get("ENV", "tp"),
}
)
# === Tracing ===
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint, insecure=True))
)
trace.set_tracer_provider(tracer_provider)
# === Logging ===
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint, insecure=True))
)
set_logger_provider(logger_provider)
# Bridge: redirigí el módulo `logging` standard a OTel.
# Esto significa que todos los `logger.info(...)` del scraper salen via OTLP
# SIN cambiar ni una línea de los call-sites.
handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
logging.basicConfig(level=logging.INFO, handlers=[handler])5.3 — Diff con el
logging_setup.py viejo
Para que vean exactamente qué cambia (y qué NO cambia):
--- logging_setup.py (TP 2 · P1, JSON a stdout)
+++ otel_setup.py (TP 2 · P3, OTLP a collector)
@@ -1,18 +1,38 @@
import logging
-from logging.handlers import RotatingFileHandler
-from pythonjsonlogger.json import JsonFormatter
+import os
+from opentelemetry._logs import set_logger_provider
+from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
+from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
+from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
+from opentelemetry.sdk.resources import Resource
-def setup_logging(log_file="output/scraper.log"):
- json_formatter = JsonFormatter("%(asctime)s %(levelname)s %(name)s %(message)s")
- stream_handler = logging.StreamHandler()
- stream_handler.setFormatter(json_formatter)
+def setup_otel(service_name: str = "scraper"):
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317")
+ resource = Resource.create({"service.name": service_name})
- file_handler = RotatingFileHandler(log_file, maxBytes=2_000_000, backupCount=3)
- logging.basicConfig(level=logging.INFO, handlers=[stream_handler, file_handler])
+ logger_provider = LoggerProvider(resource=resource)
+ logger_provider.add_log_record_processor(
+ BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint, insecure=True))
+ )
+ set_logger_provider(logger_provider)
+
+ handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
+ logging.basicConfig(level=logging.INFO, handlers=[handler])🎯 Lo que NO cambia: ninguno de los
logger.info("Scrape iniciado", extra={"producto": producto, ...})del Hit #5 del TP 2 · Parte 1. El código de negocio queda idéntico. Eso es el punto de OTel — la instrumentación es transparente al código de la app.
5.4 — Configurar la env var en el Pod
otel/manifests/scraper-otlp-config.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: otel-collector-endpoint
namespace: ml-scraper
data:
# Apunta al collector DaemonSet del mismo nodo (status.hostIP del pod)
# Más resiliente que apuntar al Service: si el collector del nodo se cae,
# el pod escalado en otro nodo apunta a su collector local.
OTEL_EXPORTER_OTLP_ENDPOINT: "http://$(NODE_IP):4317"
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
OTEL_SERVICE_NAME: "scraper"Modificar el CronJob del scraper (TP 1 · P2 Hit #7) para
sumar la env NODE_IP desde el fieldRef y montar el
ConfigMap como envFrom:
# scraper-cronjob.yaml (extracto del cambio)
spec:
containers:
- name: scraper
image: ghcr.io/<usuario>/scraper:otel-v1
env:
- name: NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
envFrom:
- configMapRef:
name: otel-collector-endpoint5.5 — Re-publicar imagen y aplicar
# Build + push (usar el Hit #6 del TP 1 · Parte 2 — workflow de CI ya existe)
docker build -t ghcr.io/<usuario>/scraper:otel-v1 .
docker push ghcr.io/<usuario>/scraper:otel-v1
kubectl apply -f otel/manifests/scraper-otlp-config.yaml
kubectl apply -f scraper-cronjob.yaml
kubectl -n ml-scraper create job --from=cronjob/scraper-hourly scraper-sdk-test-1
kubectl -n ml-scraper wait --for=condition=complete job/scraper-sdk-test-1 --timeout=600sOutput esperado del Hit #5
En Loki y en Kibana, los logs del Job scraper-sdk-test-1
deben aparecer con un trace_id y
span_id no vacíos en cada log record. Esos campos
son lo que distingue un log emitido via SDK de uno parseado del archivo.
Query LogQL para verificar:
{service="scraper", k8s_job_name="scraper-sdk-test-1"} | json | trace_id != ""
Si aparecen logs y trace_id está populado, el SDK está
exportando correctamente.
⚠️ Pitfall conocido: el
BatchLogRecordProcessoragrupa logs antes de exportarlos. Si el scraper termina muy rápido (segundos), pueden perderse los últimos logs que quedaron en el buffer. Llamar siemprelogger_provider.shutdown()antes de salir — la doc oficial recomienda registrarlo viaatexit.register(...). En elotel_setup.pyde arriba lo agregaron como ejercicio.
Hit #6 — Bonus: Traces (opcional, +5 %)
Si el SDK del Hit #5 ya está adentro, sumar traces es trivial: el
TracerProvider ya está registrado, solo falta
anotar las funciones del scraper que queremos ver como
spans, y desplegar un backend de traces.
6.1 — Backend: Jaeger all-in-one
helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm install jaeger jaegertracing/jaeger \
--version 3.4.x \
--namespace otel \
--set provisionDataStore.cassandra=false \
--set provisionDataStore.elasticsearch=false \
--set storage.type=memory \
--set agent.enabled=false \
--set collector.enabled=true \
--set query.enabled=true \
--set query.service.type=NodePort \
--set query.service.nodePort=30002💡 Storage in-memory: para el TP. En producción se usa Elasticsearch o Cassandra. El TP no requiere persistencia.
6.2 — Sumar exporter
otlp/jaeger al collector
En el OpenTelemetryCollector:
exporters:
# ... otlphttp/loki, elasticsearch ...
otlp/jaeger:
endpoint: jaeger-collector.otel.svc.cluster.local:4317
tls:
insecure: true
service:
pipelines:
logs:
receivers: [filelog, otlp]
processors: [k8sattributes, attributes, transform, batch]
exporters: [otlphttp/loki, elasticsearch]
# === NUEVA pipeline para traces ===
traces:
receivers: [otlp]
processors: [k8sattributes, batch]
exporters: [otlp/jaeger]6.3 —
Auto-instrumentar requests y urllib3 (las libs
de Selenium)
scraper-instrumentation/requirements-otel.txt:
opentelemetry-instrumentation-requests==0.51b0
opentelemetry-instrumentation-urllib3==0.51b0
⚠️ Versionado de instrumentations: las instrumentation libs de Python están en
0.Xmientras que la API/SDK están en1.X. Es a propósito — la spec considera las instrumentations menos estables. La regla: la versión de instrumentation debe matchear la de la API. Paraopentelemetry-api==1.30.0la instrumentation correspondiente es0.51b0.
En otel_setup.py, sumar al final del
setup_otel():
# Auto-instrumentation de requests y urllib3
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
RequestsInstrumentor().instrument()
URLLib3Instrumentor().instrument()Y en los call-sites del scraper, envolver explícitamente los bloques que querés ver como span:
# extractors.py
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def scrape_producto(producto: str):
with tracer.start_as_current_span(
"scrape_producto",
attributes={"producto": producto},
) as span:
# ... lógica de scraping ...
span.set_attribute("results.count", len(results))
return results6.4 — Verificar en Jaeger UI
Abrir http://<node-ip>:30002 (Jaeger UI) →
seleccionar service scraper → Find Traces. Tiene que
aparecer una traza tipo:
scrape_producto (1.4 s)
├── HTTP GET https://articulo.mercadolibre.com.ar/... (selenium → requests) (1.1 s)
│ └── HTTP GET https://articulo.mercadolibre.com.ar/... (urllib3) (1.1 s)
└── parse_results (62 ms)
Si la ven, está armado. Capturen screenshot en
otel/screenshots/hit6-trace-jaeger.png.
⚠️ Selenium WebDriver no genera spans automáticos — Selenium usa internamente requests/urllib3 cuando habla con el chromedriver via JSON-Wire, pero sus operaciones high-level (
driver.get(),find_element) no están auto-instrumentadas. Si querés ver el flow real del scrape como un solo span, tenés que envolverlo manualmente como mostramos arriba.
Cómo entregar
- Push final al repo público (mismo repo del TP 1 / TP 2 · P1 y P2, no abrir uno nuevo) antes del 09/05/2026 23:59 ART.
- README raíz actualizado con una sección nueva “TP 2
· Parte 3 — OpenTelemetry”:
- Cómo ejecutar
otel/install.shdesde cero (con Loki + EFK ya levantados de las Partes 1 y 2). - Variables de entorno requeridas (heredadas de las partes anteriores:
GRAFANA_ADMIN_PASSWORD,ELASTIC_PASSWORD). - Link al
otel/README.mdcon los detalles.
- Cómo ejecutar
- Carpeta
otel/completa según estructura obligatoria. docs/adr/0010-instrumentacion-vendor-neutral.md(y opcionalmente0011-traces-vs-solo-logs.mdsi hicieron Hit #6).- Carpeta
otel/screenshots/con mínimo:hit2-debug-output.png— output del exporterdebugcon atributos k8s enriquecidos.hit3-fanout-loki.png— Grafana mostrando un log con sulog_id.hit3-fanout-elastic.png— Kibana mostrando el mismolog_id.hit4-old-agents-down.png— Promtail + Fluent Bit enreplicas=0, OTel collector activo, último log en los 2 backends.hit5-otlp-trace-id.png— Grafana mostrando logs contrace_idpopulado (prueba del SDK).- (bonus Hit #6)
hit6-trace-jaeger.png— traza completa del scraper en Jaeger.
- Video corto (3-5 min) mostrando:
install.shcorriendo, Promtail/Fluent Bit escalando a 0, un Job nuevo del scraper, mismo log apareciendo en Grafana Y en Kibana en simultáneo (esto es lo que la cátedra mira primero). - Mensaje en el canal Discord de la materia con el link al repo y al video.
📡 Canal Discord (consultas + entregas): https://discord.com/channels/1482135908508500148/1482135909456679139 Antes de pedir ayuda con OTel, revisá la sección Common pitfalls abajo — la mayoría de los problemas son los 5 ahí descritos.
Auto-verificación previa a la entrega
Igual que en TP 2 · Partes 1 y 2: si algo de esta lista falla, no entregues.
1)
install.sh corre limpio sobre Loki + EFK ya instalados
# Loki + EFK siguen corriendo del TP 2 · P1 y P2
kubectl get pods -A | grep -E 'loki|elasticsearch|kibana'
# Borrar el namespace otel para arrancar limpio
kubectl delete namespace otel --wait=true || true
# Re-instalar OTel from scratch
cd otel && ./install.sh
# El script DEBE terminar con exit 0 y los ✓ del Hit #12) El operator + el collector están Running
kubectl -n otel-operator-system get pods
kubectl -n otel get otelcol,ds,pod
# Esperado:
# otelcol/agent READY 1/1
# ds/agent-collector DESIRED == READY (1 si k3s single-node)3) El collector recibe logs del scraper
kubectl -n ml-scraper create job --from=cronjob/scraper-hourly verify-otel
kubectl -n ml-scraper wait --for=condition=complete job/verify-otel --timeout=600s
kubectl -n otel logs ds/agent-collector --since=2m | grep -c k8s.namespace.name
# Esperado: > 04) Mismo log_id en Loki Y en Elastic
Disparar un Job nuevo, copiar un log_id aleatorio del
output de Loki, buscarlo en Kibana. Tiene que aparecer en los dos.
5) Promtail/Alloy y Fluent Bit están en 0
kubectl -n observability get ds promtail -o jsonpath='{.status.desiredNumberScheduled}'
# Esperado: 0
kubectl -n elastic get ds fluent-bit -o jsonpath='{.status.desiredNumberScheduled}'
# Esperado: 06) El scraper exporta OTLP directo (no via filelog)
kubectl -n ml-scraper logs job/verify-otel | head -5
# Tiene que NO haber logs JSON en stdout — el SDK los emite via OTLP, no a stdout
# Si seguís viendo JSON en stdout, el setup_otel() no se está llamandoEn Loki: {service="scraper"} | json | trace_id != ""
debe devolver records.
7) RBAC del collector OK
kubectl auth can-i list pods --as=system:serviceaccount:otel:otel-collector
# Esperado: yes
kubectl auth can-i list nodes --as=system:serviceaccount:otel:otel-collector
# Esperado: yes8) gitleaks no
detecta secrets
gitleaks detect --no-git --verbose
# Esperado: 0 leaks. Si encuentra el password de Elastic en algún YAML, está mal —
# borralo y movelo a un Secret con secretKeyRef.Common pitfalls
unknown exporter "loki"
o unknown exporter "elasticsearch"
Bajaron otel/opentelemetry-collector (core) en lugar de
otel/opentelemetry-collector-contrib. Los exporters
de Loki y Elasticsearch están solo en contrib. Cambien la
imagen y kubectl apply de nuevo. La diferencia de tamaño no
importa en este TP.
k8sattributes no
enriquece
Casi siempre es RBAC. El processor consulta el API server para
resolver pod → metadata. Si el ServiceAccount no puede listar
pods/namespaces, los atributos quedan vacíos pero el collector NO
crashea — solo verás el Body del log sin los
k8s.* adjuntos. Verificar con:
kubectl auth can-i list pods --as=system:serviceaccount:otel:otel-collectorSi dice no, falta aplicar el rbac.yaml del
Hit #2.
Cardinality explosion en Loki via OTel
Cuando OTel exporta a Loki, cada attribute se
vuelve un label de Loki por default. Si tu app emite un
extra={"trace_id": "...", "request_id": "..."} como antes,
esos campos van a explotar la cardinality de Loki igual que en el TP 2 ·
Parte 1. Solución: en el exporter
otlphttp/loki, configurá explícitamente cuáles atributos
van como label vs cuáles van en el body. Loki 3.x con OTLP nativo lo
maneja mejor que el exporter legacy, pero seguí siendo conservador (≤ 10
labels).
BatchLogRecordProcessor
pierde los últimos logs
El batch processor agrupa records por tiempo (default: 5s) o tamaño
(default: 512). Si el scraper termina antes del flush, esos logs se
pierden. Siempre llamen logger_provider.shutdown()
al final, idealmente vía atexit:
import atexit
atexit.register(logger_provider.shutdown)
atexit.register(tracer_provider.shutdown)El collector arranca pero no exporta (silencioso)
Si en el YAML el nombre de un processor o exporter está mal escrito, el collector arranca pero silenciosamente lo ignora. Verificar siempre los logs del collector buscando WARNINGs:
kubectl -n otel logs ds/agent-collector | grep -i 'warn\|error'Si ves failed to find exporter o
processor not found, ahí está el typo.
otlphttp/loki
falla con HTTP 404
Si Loki está pinneado en una versión < 3.0, no tiene el
endpoint /otlp. Hay dos caminos:
- Upgradear Loki a ≥ 3.0 (chart
grafana/loki≥ 6.x). - Usar el exporter
lokilegacy en lugar deotlphttp/loki— funciona pero pierde estructura. No recomendado en 2026.
Verificar versión de Loki:
kubectl -n observability exec sts/loki -- /usr/bin/loki -versionCriterios de evaluación
Requisitos bloqueantes (no se acepta la entrega sin estos)
Estos no suman puntos — son condición necesaria para que la entrega sea corregible. Si falta cualquiera, la nota es 0.
- TP 2 · Partes 1 y 2 entregadas y aprobadas (mínimo 60/100 cada una). Loki + EFK tienen que estar arriba al momento de evaluar la Parte 3.
otel/install.shfunciona en un cluster con Loki + EFK preexistentes (verificado en auto-verificación #1).- Helm charts pinneados a versiones específicas (no
latest). - Distribución
contribdel collector (nocore). - Sin secrets en el repo
(
gitleaks detectda 0 leaks). - Auto-verificación completa ejecutada antes del push final.
Tabla de puntaje (100 %)
| Criterio | Peso |
|---|---|
| Hit #1 — OpenTelemetry Operator desplegado + CRDs presentes | 10 % |
Hit #2 — OpenTelemetryCollector en
modo DaemonSet leyendo logs + k8sattributes
enriqueciendo |
15 % |
Hit #3 — Fan-out simultáneo a Loki + Elasticsearch
verificado con log_id matching |
25 % |
| Hit #4 — Promtail + Fluent Bit escalados a 0, flujo sigue funcionando solo con OTel | 15 % |
Hit #5 — Scraper instrumentado con SDK Python,
trace_id populado en log records |
20 % |
ADR
0010-instrumentacion-vendor-neutral.md
justificando OTel vs status quo |
15 % |
Bonus Hit #6 — Traces visibles en Jaeger + ADR
0011 |
+5 % |
📊 Por qué Hit #3 pesa 25 %: es el corazón del TP. Demuestra la propiedad que justifica OTel en un proyecto real (multi-backend simultáneo). Si el fan-out no funciona, el resto del TP es paja con poca chicha.
Material de apoyo
Diagrama ASCII de los 3 stacks corriendo en paralelo
Estado final esperado tras la Parte 3:
┌─────────────────────────────────────────────────────────────────────────────┐
│ k3s/k3d cluster │
│ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ namespace: ml-scraper │ │
│ │ │ │
│ │ ┌───────────────────────────────────────────────────────────┐ │ │
│ │ │ scraper (Pod, CronJob hourly + Job manual) │ │ │
│ │ │ ┌─────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Python app + OTel SDK 1.30.x │ │ │ │
│ │ │ │ - LoggerProvider → BatchLogRecordProcessor │ │ │ │
│ │ │ │ - TracerProvider → BatchSpanProcessor │ │ │ │
│ │ │ └────────────┬────────────────────────────────────┘ │ │ │
│ │ └────────────────┼──────────────────────────────────────────┘ │ │
│ └────────────────────┼─────────────────────────────────────────────────┘ │
│ │ OTLP/gRPC :4317 │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │ namespace: otel │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ OpenTelemetryCollector (DaemonSet, mode: agent) │ │ │
│ │ │ │ │ │
│ │ │ receivers: filelog (/var/log/pods/*) + otlp │ │ │
│ │ │ processors: batch + k8sattributes + transform │ │ │
│ │ │ exporters: otlphttp/loki + elasticsearch + │ │ │
│ │ │ otlp/jaeger (bonus) │ │ │
│ │ └─────┬──────────────────┬────────────────────┬──────────────┘ │ │
│ └─────────┼──────────────────┼────────────────────┼────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ ns: observability│ │ ns: elastic │ │ ns: otel (bonus) │ │
│ │ │ │ │ │ │ │
│ │ Loki 3.x │ │ Elasticsearch │ │ Jaeger all-in-one │ │
│ │ + Grafana 11 │ │ + Kibana │ │ (in-memory storage) │ │
│ │ │ │ │ │ │ │
│ │ :30000 │ │ :30001 │ │ :30002 │ │
│ └──────────────────┘ └──────────────────┘ └──────────────────────┘ │
│ │
│ [APAGADOS — escalados a 0] │
│ - DaemonSet promtail (ns: observability) │
│ - DaemonSet fluent-bit (ns: elastic) │
└────────────────────────────────────────────────────────────────────────────┘
Tabla de receivers / exporters útiles
Receivers más usados
| Receiver | Para qué | Notas |
|---|---|---|
otlp |
OTLP/gRPC + HTTP entrantes | El default — apps con SDK lo usan |
filelog |
Leer archivos de log | Usado en Hit #2 para /var/log/pods/* |
hostmetrics |
CPU, memory, disk del host | Útil para métricas de nodos |
kafka |
Consumir de un topic | Para apps que ya emiten a Kafka |
prometheus |
Scrape de endpoints /metrics |
Reemplaza a Prometheus si querés OTLP-only |
k8s_cluster |
Eventos del API server | Para auditoría |
k8s_events |
k8s events (kubectl get events) | Útil para detectar OOMKilled, evictions |
Exporters más usados
| Exporter | Backend | Notas |
|---|---|---|
otlphttp |
Cualquier backend OTLP | El más universal — reemplazó al loki,
prometheus, etc. |
otlp |
Otro collector (gateway pattern) | Para HA / fan-in |
elasticsearch |
Elasticsearch | Lo usado en Hit #3 |
prometheus / prometheusremotewrite |
Prometheus | Para integraciones legacy |
kafka |
Kafka topic | Para arquitecturas de streaming |
debug |
Stdout del propio collector | Indispensable para debuggear — primer exporter que prueban siempre |
file |
Archivo local | Útil para offline replay / forensics |
awscloudwatchlogs / googlecloud /
azuremonitor |
Cloud nativo | Si están en cloud |
Esqueleto de
install.sh
#!/usr/bin/env bash
set -euo pipefail
NAMESPACE=otel
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "→ Verificando pre-requisitos: Loki + EFK"
kubectl -n observability get svc loki >/dev/null \
|| { echo "✗ Loki no encontrado en ns observability — TP 2 · P1 no entregado"; exit 1; }
kubectl -n elastic get svc elasticsearch-master >/dev/null \
|| { echo "✗ Elasticsearch no encontrado en ns elastic — TP 2 · P2 no entregado"; exit 1; }
echo "→ Namespace + Helm repo"
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts >/dev/null 2>&1 || true
helm repo update >/dev/null
echo "→ cert-manager (si no está)"
kubectl get ns cert-manager >/dev/null 2>&1 || helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.16.1 --set installCRDs=true --wait --timeout 5m
echo "→ OpenTelemetry Operator"
helm upgrade --install otel-operator open-telemetry/opentelemetry-operator \
--version 0.74.0 \
--namespace otel-operator-system --create-namespace \
--values "$DIR/helm/otel-operator-values.yaml" \
--wait --timeout 5m
echo "→ RBAC y secrets"
kubectl apply -f "$DIR/manifests/rbac.yaml"
kubectl get secret elastic-credentials -n elastic -o yaml \
| sed 's/namespace: elastic/namespace: otel/' \
| grep -v 'creationTimestamp\|resourceVersion\|uid\|selfLink' \
| kubectl apply -f -
echo "→ OpenTelemetryCollector CRD"
kubectl apply -f "$DIR/manifests/collector-agent.yaml"
kubectl -n "$NAMESPACE" wait --for=condition=ready otelcol/agent --timeout=180s
echo "→ Apagar agentes legacy"
kubectl -n observability scale ds/promtail --replicas=0 || true
kubectl -n elastic scale ds/fluent-bit --replicas=0 || true
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
echo ""
echo "✓ OpenTelemetry Operator running"
echo "✓ OpenTelemetryCollector CRD aplicado"
echo "✓ Collector DaemonSet running"
echo "✓ Pipeline activo: filelog + otlp → batch → k8sattributes → [loki, elasticsearch]"
echo "✓ Promtail (observability) escalado a 0"
echo "✓ Fluent Bit (elastic) escalado a 0"
echo ""
echo "→ Verificá fan-out:"
echo " Grafana: http://${NODE_IP}:30000 → Explore → Loki"
echo " Kibana: http://${NODE_IP}:30001 → Discover → scraper-logs-*"
echo " Jaeger: http://${NODE_IP}:30002 (si hicieron Hit #6)"chmod +x install.sh antes de commitearlo.
Esqueleto de
ADR 0010-instrumentacion-vendor-neutral.md
Mismo formato Michael Nygard. Ejemplo concreto:
# 0010 — Adoptamos OpenTelemetry como capa de instrumentación vendor-neutral
- Date: 2026-05-25
- Status: Accepted
- Deciders: <equipo>
## Contexto
Tras los TP 2 · Partes 1 y 2 tenemos dos stacks completos de logging corriendo en
paralelo: Loki (con Promtail) y Elasticsearch (con Fluent Bit). Ambos funcionan,
pero:
- Estamos corriendo dos DaemonSets que leen los mismos archivos.
- El scraper usa `python-json-logger` que es un detalle de implementación
específico — si mañana migráramos a Datadog o New Relic, habría que cambiar el
módulo de logging.
- La decisión "Loki vs Elastic" se siente prematura y arbitraria.
OpenTelemetry es un proyecto CNCF graduated (2023) que define un protocolo
(OTLP) y SDKs vendor-neutral. Los 4 grandes proveedores SaaS (Datadog, New
Relic, Dynatrace, Splunk) y los OSS (Loki, Elasticsearch, Prometheus, Jaeger,
Tempo) soportan OTLP nativo en 2026.
## Decisión
Adoptamos **OpenTelemetry Collector + SDK** como capa de instrumentación
unificada. El collector hace fan-out a Loki Y a Elasticsearch en simultáneo
(prueba de concepto que el modelo funciona). El scraper se re-instrumenta con
el SDK de OTel para Python.
## Consecuencias
- Más fácil: un solo agente por nodo (DaemonSet del collector). Sumar un backend
nuevo es 5 líneas de YAML. El código del scraper no cambia (call-sites
idénticos al TP 2 · P1).
- Más difícil: una capa más de YAML para mantener (CRDs, config del collector).
El SDK de Python es menos maduro que Java/Go — esperar bugs ocasionales.
Aprender OTLP, processors, OTTL.
- Sacrificio: más latencia de export (batch processor) vs hablar HTTP/JSON
directo a Loki — pero medible solo en sub-ms, irrelevante en un scraper.
- Riesgo: el SDK de Python aún tiene la API de logs marcada "stable since 2024"
pero todavía con cambios menores entre minor versions. Pinear `==1.30.x`.
## Referencias
- OTel CNCF graduation: https://www.cncf.io/announcements/2023/11/06/cloud-native-computing-foundation-announces-opentelemetry-graduation/
- OTLP spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md
- Comparativa: TP 2 · Parte 4 (ADR comparativo final)Referencias y Bibliografía
Solo lo directamente vinculado a lo que se les pide en este TP. Los libros generales de Kubernetes / observabilidad viven en TP 0 / TP 2 · P1 y no se repiten.
Hit #1 — OpenTelemetry Operator
- OpenTelemetry Operator — README — diseño del operator, CRDs definidos, modos de deploy. https://github.com/open-telemetry/opentelemetry-operator
- Helm chart
opentelemetry-operator— values.yaml — toda la sintaxis del chart: webhooks, resources, watch namespace. https://github.com/open-telemetry/opentelemetry-helm-charts/tree/main/charts/opentelemetry-operator - cert-manager docs — por qué los webhooks admission necesitan TLS y cómo cert-manager lo resuelve. https://cert-manager.io/docs/installation/helm/
Hit #2 — Collector como CRD + receivers/processors
- OpenTelemetryCollector CRD reference — todas las keys del spec del CRD (mode, env, volumeMounts, config). https://github.com/open-telemetry/opentelemetry-operator/blob/main/docs/api.md
- OTel Collector — filelog receiver — config, operators (parser container, regex, json), file rotation handling. https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver
- OTel Collector — k8sattributes processor — cómo enriquece los logs/spans con metadata k8s, qué RBAC requiere. https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/k8sattributesprocessor
- OTel Collector — batch processor — el processor más
usado. Config de
timeout,send_batch_size,send_batch_max_size. https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/batchprocessor
Hit #3 — Fan-out Loki + Elasticsearch
- Loki — OTLP ingestion (>= 3.0) — el endpoint
/otlpque reemplaza al exporterlokilegacy. https://grafana.com/docs/loki/latest/send-data/otel/ - OTel Collector — elasticsearch exporter — config
completa:
endpoints,tls,mapping.mode(ecs vs raw vs otel),logs_index. https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/elasticsearchexporter - OTel Collector — otlphttp exporter — el exporter
universal HTTP/protobuf. Lo usamos como
otlphttp/loki. https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/otlphttpexporter - OTTL — OpenTelemetry Transform Language — la DSL
que usamos en el processor
transformpara inyectarlog_id. Sintaxis y funciones disponibles (UUID(),Concat(), etc.). https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/ottl
Hit #5 — Python SDK + LoggingHandler
- opentelemetry-python — Logs SDK —
LoggerProvider,LoggingHandler,BatchLogRecordProcessor. Marcado stable desde 2024. https://opentelemetry-python.readthedocs.io/en/stable/sdk/_logs.html - opentelemetry-python — OTLP gRPC log exporter — el
exporter que usamos. Config de
endpoint,insecure,headers,timeout. https://opentelemetry-python.readthedocs.io/en/stable/exporter/otlp/otlp.html - OTel — Resource semantic conventions — claves
estandarizadas para
service.name,host.name,deployment.environment. https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md
Hit #6 — Tracing (bonus)
- opentelemetry-python — Tracing API —
tracer.start_as_current_span,set_attribute,record_exception. https://opentelemetry-python.readthedocs.io/en/stable/api/trace.html - OTel instrumentation-requests / urllib3 — auto-instrumentation de HTTP clients de Python, versionado y compatibility matrix. https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation
- Jaeger Helm chart — chart
jaegertracing/jaegervalores: storage in-memory vs persistente, query NodePort, agent disabled. https://github.com/jaegertracing/helm-charts - Sigelman, B. et al. (2010). “Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”. Google Technical Report. — el paper fundacional del tracing distribuido. La clave para entender por qué un span es lo que es. https://research.google/pubs/pub36356/
OpenTelemetry — fundamentos
- OpenTelemetry — Documentation — entry point oficial. Ahí están los tutoriales por lenguaje y los conceptos generales (signals, context propagation, semantic conventions). https://opentelemetry.io/docs/
- OpenTelemetry Specification — la spec normativa.
Lectura densa, pero la única fuente de verdad para temas como
traceparent, OTLP wire format, instrumentación. https://github.com/open-telemetry/opentelemetry-specification - CNCF — OpenTelemetry graduation announcement (2023) — el contexto histórico: por qué OTel era el estándar de facto antes de la graduación oficial. https://www.cncf.io/announcements/2023/11/06/cloud-native-computing-foundation-announces-opentelemetry-graduation/
- Beyer, B., Murphy, N., Rensin, D., et al. (2018). The Site Reliability Workbook. O’Reilly. Cap. 5: “Alerting on SLOs”. — por qué la consolidación de las 3 señales (logs/metrics/traces) en una sola herramienta importa para alerting basado en SLOs. https://sre.google/workbook/alerting-on-slos/
Honestidad técnica — limitaciones de OTel
- OpenTelemetry Python — Maturity matrix — cuáles APIs están stable / experimental / deprecated por lenguaje. Útil para decidir si OTel está listo para tu lenguaje hoy. https://opentelemetry.io/docs/specs/status/
- “Why is OpenTelemetry so complex?” — Honeycomb blog (2024) — análisis honesto de la complejidad del proyecto desde un proveedor que vive de OTel. https://www.honeycomb.io/blog/opentelemetry-developer-experience