Múlt héten örököltem egy klasztert, amin nagyjából 40 mikroszerviz futott. Observability gyakorlatilag nem volt: alap Prometheus metrikák és pár elszórt logsor. A csapat “a következő sprintre” akart distributed tracinget. Két hét alatt, tucatnyi repót érintve, kódmódosítással? Teljesen irreális.

Ezért az OpenTelemetry Operator auto-instrumentációját választottam. Ez történt a gyakorlatban.

A kiindulás

Kubernetes 1.31 fut EKS-en. A cél egyszerű volt: trace-ek és metrikák minden szervizből Grafana Tempóba és Mimirbe, alkalmazáskód-módosítás nélkül.

A szervizek vegyesek voltak: Python (FastAPI), Node.js (Express), Go, és pár Java Spring Boot alkalmazás.

Az OTel Operator telepítése

Először az OpenTelemetry Operator. Helmel tettem fel, mert pinelni akartam a verziót:

helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update

helm install otel-operator open-telemetry/opentelemetry-operator \
  --namespace otel-system \
  --create-namespace \
  --set manager.collectorImage.repository=otel/opentelemetry-collector-k8s \
  --set admissionWebhooks.certManager.enabled=true \
  --version 0.74.0

Fontos: kell a cert-manager. Ha nincs fent, a webhook tanúsítványok elhasalnak, és a pod admission megakad. Ezen buktam 15 percet, mert azt hittem, már telepítve van. Nem volt.

# Nézzük, fut-e a cert-manager
kubectl get pods -n cert-manager

# Ha nincs:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.16.3/cert-manager.yaml

A Collector

Következő lépésként deployoltam egy collectort. DaemonSet módot választottam, hogy minden node a saját telemetriáját kezelje. Íme az OpenTelemetryCollector CR:

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-collector
  namespace: otel-system
spec:
  mode: daemonset
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
    processors:
      batch:
        timeout: 5s
        send_batch_size: 1024
      memory_limiter:
        check_interval: 1s
        limit_mib: 512
        spike_limit_mib: 128
      k8sattributes:
        extract:
          metadata:
            - k8s.namespace.name
            - k8s.deployment.name
            - k8s.pod.name
            - k8s.node.name
        pod_association:
          - sources:
              - from: resource_attribute
                name: k8s.pod.ip
    exporters:
      otlphttp/tempo:
        endpoint: http://tempo.monitoring:4318
      prometheusremotewrite:
        endpoint: http://mimir.monitoring:9009/api/v1/push
    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlphttp/tempo]
        metrics:
          receivers: [otlp]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [prometheusremotewrite]

A k8sattributes processzor itt a fő nyereség. Minden spanhez és metrikához automatikusan hozzáadja a pod nevet, namespace-t, deploymentet és node-ot. Akkor értékeled igazán, amikor egy trace-ben csak annyit látsz: “service unknown.”

Auto-instrumentációs erőforrások

Itt jön a lényeg. Az operátor pod admission közben injektál instrumentációt. Nyelvenként létrehozol egy Instrumentation CR-t:

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: otel-instrumentation
  namespace: otel-system
spec:
  exporter:
    endpoint: http://otel-collector.otel-system:4317
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "0.25"
  python:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.50b0
    env:
      - name: OTEL_PYTHON_LOG_CORRELATION
        value: "true"
  nodejs:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:0.56.0
  java:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:2.12.0
  go:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-go:0.19.0-alpha

Utána annotálod a podokat, vagy akár a teljes namespace-t, ha bevállalós vagy:

# Deploymentenként
metadata:
  annotations:
    instrumentation.opentelemetry.io/inject-python: "otel-system/otel-instrumentation"

Vagy namespace szinten mindenre:

# Magát a namespace-t annotáljuk
kubectl annotate namespace production \
  instrumentation.opentelemetry.io/inject-python="otel-system/otel-instrumentation" \
  instrumentation.opentelemetry.io/inject-nodejs="otel-system/otel-instrumentation" \
  instrumentation.opentelemetry.io/inject-java="otel-system/otel-instrumentation"

Annotálás után újra kell indítani a deploymenteket. Az injektálás csak pod létrehozáskor történik:

kubectl rollout restart deployment -n production --selector=app

Mi tört el

A Go instrumentáció még alpha

A Go auto-instrumentáció eBPF-et használ, és privileged security contextet igényel. EKS-en, szigorú PodSecurityStandard mellett, egyszerűen nem indult el. Végül kihagytam a Go szervizeket, és később manuális instrumentációt tervezek az OTel Go SDK-val. Nem tökéletes, de ezek többnyire belső utility szervizek.

Python init container OOM

A Python auto-instrumentáció init containere csomagokat próbált telepíteni egy megosztott volume-ra. Két szervizünknél hatalmas dependency fa volt, és az init container elérte a 256Mi memória limitet. Javítás:

python:
  image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.50b0
  resources:
    limits:
      memory: 512Mi

Node.js ESM modulok

Három Node.js szervizünk ESM-et használt ("type": "module" a package.json-ben). Az auto-instrumentációs loader ezt rosszul kezeli. Csendben elhasal: nincs trace, nincs értelmes hibaüzenet. Egy óráig a collectort hibáztattam, mire kiderült.

A megoldás a node options beállítása az ESM loaderrel:

nodejs:
  env:
    - name: NODE_OPTIONS
      value: "--require @opentelemetry/auto-instrumentations-node/register --experimental-loader=@opentelemetry/instrumentation/hook.mjs"

Ez még mindig nem fed le minden ESM esetet. A két szervizhez, ahol ez sem segített, három sor manuális instrumentációt adtunk az entry pointba.

Sampling zavar

A samplert parentbased_traceidratio-ra állítottam 0.25-tel (25%). Amit elsőre benéztem: head-based samplingnél, service mesh mellett, a belépő szerviz dönt, és a downstream szervizek öröklik a döntést. Ez teljesen helyes működés, csak a csapat minden requestet látni akart, ezért zavaró volt. Debugoláshoz érdemes 1.0-ra állítani, majd később visszavenni.

Az eredmények

Kb. 20 perc tényleges setup után (plusz egy óra ESM debug):

  • 40-ből 34 szerviz küldött trace-eket a Tempóba
  • Automatikus span korreláció HTTP hívások között
  • Pod/namespace/deployment címkék minden metrikán és trace-en
  • P95 latency dashboardok Grafanában egy órán belül

A 6 hiányzó szerviz: 4 Go (alpha eBPF probléma), 2 Node.js ESM edge case, amihez manuális SDK setup kellett.

Amibe belefutottam

1. Az annotáció nyelvfüggő, nem általános. Nem elég annyi, hogy “instrumentáld ezt a podot.” Tudnod kell, milyen runtime fut benne, és ahhoz tartozó annotáció kell. Ha egy pod Pythont és Node-ot is futtat (sidecar pattern), mindkettő szükséges.

2. Az init container sorrend tényleg számít. Ha a podban már vannak hálózatot igénylő init containerek, az OTel init container versenyezhet velük. Az egyik szerviznél az OTel init az Istio sidecar előtt futott, ezért induláskor nem érte el az exporter endpointot. Az app readiness probe-ban egy 5 másodperces sleep megoldotta.

3. A resource overhead valós, csak kicsi. Egy instrumentált pod kb. 30-50MB extra memóriát használ, CPU-ban pedig minimális a többlet. 34 szerviznél ez kb. 1.5GB plusz klasztermemória. Érdemes előre betervezni.

4. A verziók pinelése kritikus. Az operátor, a collector és az auto-instrumentációs image-ek verzióinak kompatibilisnek kell lenniük. Nálam volt egy mismatch az operátor (0.74.0) és egy régebbi Python image (0.45b0) között, ami csendes hibákat okozott. Mindig nézd meg az OTel Operator kompatibilitási mátrixát.

A rések megtalálása: melyik szerviz nem instrumentált?

A rollout után 40-ből 34 szerviz küldött trace-eket. De hogyan találod meg azokat, amik kimaradtak? Nekem nem volt erre dashboardom, és tovább tartott megtalálni őket, mint kellett volna.

Mostanra kialakult egy módszerem, amivel elkapom a nem instrumentált szervizeket, mielőtt vakfoltokká válnak.

Annotációk ellenőrzése a futó podokkal szemben

Ez az egysoros összehasonlítja, minek kellene instrumentáltnak lennie, és mi az ténylegesen:

# Listázd az OTel annotáció nélküli deploymenteket
kubectl get deployments -n production -o json | \
  jq -r '.items[] | select(.spec.template.metadata.annotations == null or
    ((.spec.template.metadata.annotations | keys | map(select(startswith("instrumentation.opentelemetry.io"))) | length) == 0)) |
    .metadata.name'

Ha egy deployment megjelenik itt, azt vagy szándékosan hagytuk ki, vagy elsiklottunk felette az annotáláskor.

A collector lekérdezése a csendes szervizekre

Annotált szervizek is lehetnek némák. A Python stdlib http.server, bizonyos ESM Node alkalmazások és bármi, ami nem támogatott frameworköt használ, megkapja az init containert, de nulla spant küld. A másik oldalról kell ellenőrizni.

Ha Tempót használsz, kérdezd le azokat a szervizeket, amik mostanában nem küldtek spant:

# Az elmúlt 24 órában látott szervizek
count by (service_name) (rate(traces_spanmetrics_calls_total[24h]) > 0)

Vesd össze ezt a listát az elvárt szervizneveiddel. Ha egy production szerviz nem jelenik meg a Tempóban, vagy nincs instrumentálva, vagy csendben hibás.

Coverage dashboard építése

Csináltam egy egyszerű Grafana dashboardot, ami két adatforrást köt össze:

  1. Kubernetes API (az Infinity pluginen keresztül): minden deployment listája az annotáció státuszukkal
  2. Tempo/Mimir: aktívan spant küldő szervizek listája

A panel pirossal jelöli azokat a szervizeket, amik léteznek Kubernetesben, de nincs trace adatuk. Kb. 30 percbe telt összerakni, és óráknak találgatást spórolt meg.

Frameworkök, amiket az auto-instrumentáció nem fed le

Ezt senki nem mondja el előre. Az auto-instrumentáció ismert library-k monkey-patchelésén alapul. Ha a szervized olyasmit használ, ami nincs a listán, csendben kimarad.

Python: FastAPI, Flask, Django, aiohttp, Tornado, Falcon, requests, urllib3, SQLAlchemy, psycopg2, redis, celery mind működik. A stdlib http.server nem. Egyedi async frameworkök találat vagy sem.

Node.js: Express, Koa, Hapi, Fastify és a népszerű HTTP kliensek működnek. Tiszta ESM modulok problémásak (lásd korábbi szekció). Egyedi WebSocket szerverekhez általában kézi spanek kellenek.

Java: Spring Boot, JAX-RS, Servlet, JDBC és a legtöbb népszerű framework jól működik. A Java rendelkezik a legérettebb auto-instrumentációval.

Go: Még alpha. eBPF-alapú, privilegizált containereket igényel. A legtöbb csapat kihagyja és manuális SDK-t használ.

A legbiztosabb megközelítés: az auto-instrumentáció telepítése után futtass egy szintetikus kérést minden szervizen keresztül, és nézd meg a Tempóban az eredményül kapott trace-t. Ha egy szerviz kezelte a kérést, de nem jelenik meg a trace-ben, megtaláltad a rést.

Mit csinálnék másképp

Namespace-szintű annotációkkal kezdenék stagingben. Én egyből productionbe mentem (0.25-ös samplerrel), működött, de ebben volt szerencse is. Az ESM gond simán okozhatott volna init container hibákat, amik blokkolják a deploymenteket, ha a webhook failurePolicy értéke Fail lett volna az alapértelmezett Ignore helyett.

Emellett kisebb klasztereknél Gateway módú collectort (centralizált deploymentet) használnék DaemonSet helyett. A DaemonSet node-onként egy collectort jelent, ami 5 node körül már túlzás lehet. A mi 12 node-os klaszterünknél még indokolt volt.

Összegzés

Az OpenTelemetry auto-instrumentáció Kubernetesen nagyon jól működik Python, Node.js és Java workloadoknál. A Go támogatás még érik. A setup deklaratív, az operátor tisztán kezeli az injektálást, és alkalmazáskód módosítása nélkül kapsz production-grade observabilityt.

Ha nálatok is az a mondás, hogy “majd később hozzáadjuk a tracinget”, ez egy működő kerülőút. Húsz perc alatt elindulhatnak a trace-ek, és ez sokkal jobb, mint fél év halogatás.