Gürültücü OpenTelemetry oto-enstrümantasyon kütüphanelerini susturmak

Selamlar, bu yazımda OpenTelemetry'nin oto-enstrümantasyon (auto-instrumentation) tarafında karşımıza çıkan o klasik gürültü problemine bakacağız. Yani 'paketi kurdum, env değişkenlerini verdim, span akmaya başladı' dediğiniz an ortaya çıkan ama sonra 'bunların yarısı niye burada?' dediren o durum. Lafı çok uzatmadan başlayayım.

Servisinize gelen tek bir HTTP isteği aslında 3-5 anlamlı span üretir: HTTP server span'i, veritabanı sorgusu, bir-iki downstream çağrı. Ama auto-instrumentation tam kapasite çalışırken aynı isteğin altında 20-40 span görmeniz şaşırtıcı değil. DNS lookup'ları, TCP connect'leri, config dosyası için yapılan fs okumaları, içerideki gRPC health-check'leri, Redis'e atılan PING'ler... Liste uzun. Hepsi maliyet üretiyor, çoğu hata ayıklarken kimsenin bakmadığı satırlar.

Önce gürültüyü ölçelim

Bir şey kapatmadan önce neyi kapatacağımızı bilmemiz lazım. Çoğu trace backend'i, span'leri instrumentation kütüphanesine göre gruplamamıza izin veriyor. ClickHouse üstünde basit bir sorgu kafi:

SELECT
    attributes['otel.library.name'] AS library,
    count() AS span_count,
    round(count() * 100.0 /
          (SELECT count() FROM otel_traces
           WHERE Timestamp > now() - INTERVAL 1 HOUR), 2) AS pct
FROM otel_traces
WHERE Timestamp > now() - INTERVAL 1 HOUR
GROUP BY library
ORDER BY span_count DESC;

Listenin tepesinde tipik olarak şunları görürsünüz: dns, Node tarafında fs, net, gereksiz yere açılan grpc health-check'leri, sürekli PING atan redis istemcisi. Bence bu sorguyu canlıya açtığınız ilk hafta günlük rutine almak çok şey öğretiyor.

Python tarafında kapatmak

Python'da opentelemetry-instrument komutuyla çalışıyorsanız en hızlı yol bir env değişkeni:

export OTEL_PYTHON_DISABLED_INSTRUMENTATIONS="urllib3,requests,system-metrics,threading"
opentelemetry-instrument python app.py

Daha ince ayar isterseniz programatik kurulum çok daha rahat. Aşağıda Flask + SQLAlchemy + Redis kombosu için bir örnek var; özellikle Redis'te PING/INFO/CONFIG gibi keepalive komutlarını ayrı bir isim altında topluyoruz, böylece grafiklerde gürültüyü tek satırda görüp atlayabiliyoruz:

from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.instrumentation.redis import RedisInstrumentor

FlaskInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument()

RedisInstrumentor().instrument(
    request_hook=lambda span, conn, args: (
        span.update_name('redis-internal')
        if args and args[0] in ('PING', 'INFO', 'CONFIG')
        else span.set_attribute('db.redis.command', args[0])
    ),
)

urllib3, threading ve logging instrumentation'larını bilerek açmıyoruz. İlki her outbound HTTP'yi span'e çevirir, ikincisi her thread işini, üçüncüsü ise log altyapısı ile circular bir hâle yol açabiliyor.

Node.js tarafında kapatmak

Node tarafında @opentelemetry/auto-instrumentations-node varsayılan olarak hepsini açıyor. Biz açıkça hangilerini istediğimizi söyleyelim:

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } =
  require('@opentelemetry/auto-instrumentations-node');

const sdk = new NodeSDK({
  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-dns': { enabled: false },
      '@opentelemetry/instrumentation-fs':  { enabled: false },
      '@opentelemetry/instrumentation-net': { enabled: false },
      '@opentelemetry/instrumentation-grpc': { enabled: false },
      '@opentelemetry/instrumentation-http': {
        enabled: true,
        ignoreIncomingRequestHook: (req) => {
          const path = req.url || '';
          return ['/health', '/healthz', '/ready'].includes(path);
        },
        ignoreOutgoingRequestHook: (opts) => {
          const host = opts.hostname || '';
          return host.includes('169.254.169.254');
        },
      },
      '@opentelemetry/instrumentation-pg': {
        enabled: true,
        enhancedDatabaseReporting: false,
      },
    }),
  ],
});
sdk.start();

fs ve dns'i kapatmaktan çekinmeyin; bu ikisi neredeyse her zaman gürültü kategorisinde. http tarafında ise /health gibi probe endpoint'lerini ve cloud metadata IP'lerini eleyince trace'ler bir anda okunabilir hâle geliyor.

Java tarafında kapatmak

Java agent'ı kullanıyorsanız her instrumentation'ın bir env değişkeni var. Kalıp OTEL_INSTRUMENTATION_<AdName>_ENABLED. Bir properties dosyasına koymak bence en temiz yol:

otel.instrumentation.spring-webmvc.enabled=true
otel.instrumentation.jdbc.enabled=true
otel.instrumentation.kafka-clients.enabled=true

otel.instrumentation.java-net.enabled=false
otel.instrumentation.executor.enabled=false
otel.instrumentation.reactor-netty.enabled=false
otel.instrumentation.servlet-filter.enabled=false

-Dotel.javaagent.configuration-file=otel-instrumentation.properties ile yükleyin, gerisi agent'ın işi.

Sık karşılaşılan tuzaklar

  • Hepsini birden kapatmak: Önce ölç, sonra kıs. Aksi hâlde ihtiyacınız olan bir span'i farkında olmadan susturmuş olursunuz.
  • Health check span'lerini sample tarafında elemek: Sampler'a kadar gelen span zaten üretilmiş ve büyük oranda taşınmış olur. Maliyeti baştan keserek kazanın.
  • Internal HTTP istemcisini açık bırakmak: Cloud metadata IP'sine giden çağrılar, sidecar'a giden istekler, kendi servisinize geri dönen call'lar... Bunları ignoreOutgoingRequestHook ile en başta süzün.
  • Yapılandırmayı tek servise göre kurup tüm filoya yaymak: Bir Node API'si için doğru olan ayar, fs üzerinde gerçekten iş yapan bir worker için yanlış olabilir.

Kapanış

İyi yapılandırılmış bir setup'ta trace başına span sayısının yüzde 60-80 arası düştüğünü görmek normal; bu da hem depolama maliyetinde hem de sorgu süresinde doğrudan iyileşme demek. Bana sorarsanız hedef minimum enstrümantasyon değil, faydalı enstrümantasyon: her span, bir mühendisin bir gün gerçekten bakacağı bir şey olmalı. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.