Özel Metrik Tanımlama: Netdata Plugin Yazımı

Sunucularınızı izlemek için Netdata kurduğunuzda, yerleşik metrikler çoğu zaman yeterli gelir. CPU, RAM, disk I/O, ağ trafiği… Bunların hepsi kutudan çıkar çıkmaz çalışır. Ama bir noktada fark edersiniz ki uygulamanıza özel bir şeyleri izlemeniz gerekiyor: veritabanınızdaki aktif bağlantı sayısı, kuyruk uzunluğu, belirli bir API endpoint’inin yanıt süresi ya da tamamen özel bir iş mantığına bağlı metrikler. İşte bu noktada Netdata’nın plugin sistemi devreye giriyor.

Bu yazıda sıfırdan bir Netdata plugin’i yazacağız, gerçek dünya senaryolarıyla bunu nasıl kullanabileceğinizi göstereceğiz ve production ortamında çalıştırırken dikkat etmeniz gereken noktalara değineceğiz.

Netdata Plugin Sistemi Nasıl Çalışır?

Netdata, harici plugin’leri son derece basit bir protokol üzerinden çalıştırır. Plugin, stdout’a belirli bir formatta veri yazar, Netdata bu veriyi okur ve metrik olarak kaydeder. Bu kadar. Hangi dili kullanacağınız önemli değil: Bash, Python, Go, Perl, hatta Node.js ile yazabilirsiniz.

Netdata’nın plugin’lerle iletişim protokolü şu şekilde çalışır:

  • Plugin bir döngü içinde çalışır
  • Her döngüde metrikleri toplar
  • Stdout’a belirli komutlar yazar
  • Netdata bu çıktıyı ayrıştırır ve grafiğe döker

Temel komutlar şunlardır:

  • CHART: Yeni bir grafik tanımlar
  • DIMENSION: Grafiğe bir boyut (çizgi) ekler
  • BEGIN: Veri toplama döngüsünü başlatır
  • SET: Bir dimension için değer atar
  • END: Döngüyü bitirir

Plugin dosyaları varsayılan olarak /usr/libexec/netdata/plugins.d/ dizinine konur. Netdata, bu dizindeki çalıştırılabilir dosyaları otomatik olarak başlatır.

İlk Plugin’imiz: Basit Bir Bash Script’i

Teoriyi bir kenara bırakıp direkt bir şey yazalım. MySQL/MariaDB bağlantı havuzunu izleyen basit bir plugin ile başlıyoruz.

#!/usr/bin/env bash
# /usr/libexec/netdata/plugins.d/mysql_connections.plugin

# Netdata bu değişkeni set eder, plugin buna göre uyku süresi ayarlar
NETDATA_UPDATE_EVERY=${1:-1}

# MySQL bağlantı bilgileri
MYSQL_USER="netdata_monitor"
MYSQL_PASS="gizli_sifre"
MYSQL_HOST="localhost"

# Grafik tanımını bir kere gönder
echo "CHART mysql.connections '' 'MySQL Aktif Bağlantılar' 'connections' mysql '' line 1000 ${NETDATA_UPDATE_EVERY}"
echo "DIMENSION active 'Aktif' absolute 1 1"
echo "DIMENSION max_used 'Max Kullanılan' absolute 1 1"
echo "DIMENSION max_allowed 'İzin Verilen Max' absolute 1 1"

# Ana döngü
while true; do
    # MySQL'den metrikleri çek
    RESULT=$(mysql -u"${MYSQL_USER}" -p"${MYSQL_PASS}" -h"${MYSQL_HOST}" 
        -e "SHOW STATUS WHERE Variable_name IN ('Threads_connected', 'Max_used_connections', 'max_connections');" 
        --batch --skip-column-names 2>/dev/null)

    if [ $? -eq 0 ]; then
        ACTIVE=$(echo "${RESULT}" | grep "Threads_connected" | awk '{print $2}')
        MAX_USED=$(echo "${RESULT}" | grep "Max_used_connections" | awk '{print $2}')
        MAX_ALLOWED=$(echo "${RESULT}" | grep "max_connections" | awk '{print $2}')

        echo "BEGIN mysql.connections"
        echo "SET active = ${ACTIVE:-0}"
        echo "SET max_used = ${MAX_USED:-0}"
        echo "SET max_allowed = ${MAX_ALLOWED:-0}"
        echo "END"
    fi

    sleep ${NETDATA_UPDATE_EVERY}
done

Dosyayı oluşturduktan sonra çalıştırma izni vermeyi unutmayın:

chmod +x /usr/libexec/netdata/plugins.d/mysql_connections.plugin

Netdata’yı yeniden başlatın:

systemctl restart netdata

Bu kadar. Birkaç saniye içinde Netdata arayüzünde “mysql” kategorisi altında yeni grafiğinizi görmelisiniz.

Python ile Daha Güçlü Plugin’ler

Bash iş görür ama karmaşık metrikler için Python çok daha iyi bir seçim. Netdata, Python plugin’leri için özel bir altyapı sunuyor: python.d framework’ü. Bu framework hata yönetimi, logging ve konfigürasyon dosyası desteği gibi şeyleri sizin yerinize hallediyor.

Python.d plugin’i yazmak için önce plugin dosyasını doğru yere koymanız gerekiyor:

# Python.d plugin dizini
ls /usr/libexec/netdata/plugins.d/python.d/

Şimdi gerçek bir senaryo yazalım: Bir Redis sunucusunun kuyruk uzunluğunu ve işlem hızını izleyen bir plugin.

# /usr/libexec/netdata/plugins.d/python.d/redis_queue.chart.py

import redis
from bases.FrameworkServices.SimpleService import SimpleService

# Plugin'in kaç saniyede bir çalışacağı
update_every = 5

# Grafik tanımları
charts = {
    'queue_length': {
        'options': [None, 'Redis Kuyruk Uzunlukları', 'items', 'queues', 'redis_queue.length', 'line'],
        'lines': [
            ['email_queue', 'E-posta Kuyruğu', 'absolute', 1, 1],
            ['notification_queue', 'Bildirim Kuyruğu', 'absolute', 1, 1],
            ['job_queue', 'İş Kuyruğu', 'absolute', 1, 1],
        ]
    },
    'queue_processing': {
        'options': [None, 'Kuyruk İşlem Hızı', 'items/s', 'queues', 'redis_queue.processing', 'area'],
        'lines': [
            ['processed_per_sec', 'İşlenen/sn', 'incremental', 1, 1],
        ]
    }
}


class Service(SimpleService):
    def __init__(self, configuration=None, name=None):
        SimpleService.__init__(self, configuration=configuration, name=name)
        self.order = ['queue_length', 'queue_processing']
        self.definitions = charts

        # Konfigürasyondan bağlantı bilgilerini al
        self.host = self.configuration.get('host', 'localhost')
        self.port = int(self.configuration.get('port', 6379))
        self.password = self.configuration.get('password', None)
        self.client = None

    def check(self):
        """Plugin başlarken bağlantıyı test et"""
        try:
            self.client = redis.Redis(
                host=self.host,
                port=self.port,
                password=self.password,
                socket_connect_timeout=5,
                decode_responses=True
            )
            self.client.ping()
            return True
        except Exception as e:
            self.error(f"Redis bağlantısı kurulamadı: {e}")
            return False

    def get_data(self):
        """Her döngüde bu metot çağrılır"""
        try:
            data = {}

            # Kuyruk uzunluklarını al
            data['email_queue'] = self.client.llen('queue:email') or 0
            data['notification_queue'] = self.client.llen('queue:notifications') or 0
            data['job_queue'] = self.client.llen('queue:jobs') or 0

            # Redis INFO'dan toplam işlenen komut sayısını al
            info = self.client.info('stats')
            data['processed_per_sec'] = info.get('total_commands_processed', 0)

            return data

        except redis.RedisError as e:
            self.error(f"Veri alınırken hata: {e}")
            # Bağlantıyı yeniden kur
            self.check()
            return None

Plugin için konfigürasyon dosyasını da oluşturun:

# /etc/netdata/python.d/redis_queue.conf

update_every: 5
priority: 70000
retries: 5

local:
  host: 'localhost'
  port: 6379
  password: 'redis_sifreniz'

Go ile Yüksek Performanslı Plugin

Eğer izlemeniz gereken sistem yüksek frekanslı metrik üretiyorsa ya da plugin’iniz ağır hesaplamalar yapıyorsa Go iyi bir tercih. Netdata’nın Go.d framework’ü de mevcut:

// /usr/libexec/netdata/modules/custom_app/custom_app.go

package custom_app

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// Uygulamanızın sağlık endpoint'inden dönen yapı
type AppHealthResponse struct {
    ActiveUsers    int     `json:"active_users"`
    RequestsPerSec float64 `json:"requests_per_sec"`
    ErrorRate      float64 `json:"error_rate"`
    QueueSize      int     `json:"queue_size"`
    CacheHitRate   float64 `json:"cache_hit_rate"`
}

type CustomApp struct {
    client  *http.Client
    baseURL string
}

func New(url string) *CustomApp {
    return &CustomApp{
        client: &http.Client{
            Timeout: 5 * time.Second,
        },
        baseURL: url,
    }
}

func (c *CustomApp) CollectMetrics() (*AppHealthResponse, error) {
    resp, err := c.client.Get(fmt.Sprintf("%s/health/metrics", c.baseURL))
    if err != nil {
        return nil, fmt.Errorf("HTTP isteği başarısız: %w", err)
    }
    defer resp.Body.Close()

    var health AppHealthResponse
    if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
        return nil, fmt.Errorf("JSON parse hatası: %w", err)
    }

    return &health, nil
}

Standalone Binary Plugin: En Esnek Yaklaşım

Bazen framework kullanmak istemezsiniz ya da kullanamayacağınız bir durum olur. Bu durumda doğrudan stdout’a yazan bir script veya binary yazabilirsiniz. Aşağıdaki örnek, nginx access log’unu gerçek zamanlı parse eden bir Python script’i:

#!/usr/bin/env python3
# /usr/libexec/netdata/plugins.d/nginx_log_analyzer.plugin

import sys
import time
import re
from collections import defaultdict

NETDATA_UPDATE_EVERY = int(sys.argv[1]) if len(sys.argv) > 1 else 1

# Grafik tanımları
def send_chart_definitions():
    # HTTP durum kodlarına göre istek sayısı
    print("CHART nginx.status_codes '' 'Nginx HTTP Durum Kodları' 'requests/s' nginx '' stacked 1001 {}".format(
        NETDATA_UPDATE_EVERY))
    print("DIMENSION 2xx '2xx Başarılı' incremental 1 1")
    print("DIMENSION 3xx '3xx Yönlendirme' incremental 1 1")
    print("DIMENSION 4xx '4xx İstemci Hatası' incremental 1 1")
    print("DIMENSION 5xx '5xx Sunucu Hatası' incremental 1 1")

    # Bant genişliği kullanımı
    print("CHART nginx.bandwidth '' 'Nginx Bant Genişliği' 'kilobits/s' nginx '' area 1002 {}".format(
        NETDATA_UPDATE_EVERY))
    print("DIMENSION sent 'Gönderilen' incremental 8 1000")

    sys.stdout.flush()

def parse_nginx_log(log_file, last_position):
    """Log dosyasını son okunan pozisyondan itibaren parse et"""
    counts = defaultdict(int)
    total_bytes = 0
    
    # Nginx combined log format regex
    pattern = re.compile(
        r'(?P<ip>S+) S+ S+ [.*?] ".*?" (?P<status>d{3}) (?P<bytes>d+|-)'
    )
    
    try:
        with open(log_file, 'r') as f:
            f.seek(last_position)
            for line in f:
                match = pattern.match(line)
                if match:
                    status = int(match.group('status'))
                    bytes_sent = match.group('bytes')
                    
                    # Durum kodunu kategorize et
                    if 200 <= status < 300:
                        counts['2xx'] += 1
                    elif 300 <= status < 400:
                        counts['3xx'] += 1
                    elif 400 <= status < 500:
                        counts['4xx'] += 1
                    elif 500 <= status < 600:
                        counts['5xx'] += 1
                    
                    if bytes_sent != '-':
                        total_bytes += int(bytes_sent)
            
            new_position = f.tell()
    except (IOError, OSError) as e:
        print("# Hata: {}".format(e), file=sys.stderr)
        return counts, total_bytes, last_position
    
    return counts, total_bytes, new_position

def main():
    LOG_FILE = "/var/log/nginx/access.log"
    last_position = 0
    
    # Grafik tanımlarını gönder
    send_chart_definitions()
    
    while True:
        start_time = time.time()
        
        counts, total_bytes, last_position = parse_nginx_log(LOG_FILE, last_position)
        
        # Status code verilerini gönder
        print("BEGIN nginx.status_codes")
        print("SET 2xx = {}".format(counts.get('2xx', 0)))
        print("SET 3xx = {}".format(counts.get('3xx', 0)))
        print("SET 4xx = {}".format(counts.get('4xx', 0)))
        print("SET 5xx = {}".format(counts.get('5xx', 0)))
        print("END")
        
        # Bant genişliği verilerini gönder
        print("BEGIN nginx.bandwidth")
        print("SET sent = {}".format(total_bytes))
        print("END")
        
        sys.stdout.flush()
        
        # Tam olarak NETDATA_UPDATE_EVERY saniye bekle
        elapsed = time.time() - start_time
        sleep_time = max(0, NETDATA_UPDATE_EVERY - elapsed)
        time.sleep(sleep_time)

if __name__ == "__main__":
    main()

Çalıştırma izni verin:

chmod +x /usr/libexec/netdata/plugins.d/nginx_log_analyzer.plugin

# Plugin'i test etmek için manuel çalıştırın
/usr/libexec/netdata/plugins.d/nginx_log_analyzer.plugin 1

Plugin Konfigürasyonu ve Güvenlik

Plugin’lerinizi Netdata’nın konfigürasyon sistemi üzerinden yönetmek en doğru yaklaşım. Netdata, plugin’leri netdata kullanıcısı olarak çalıştırır. Bu nedenle izin yönetimine dikkat etmeniz gerekiyor.

# netdata kullanıcısına MySQL için özel izinler ver
mysql -u root -e "
CREATE USER 'netdata_monitor'@'localhost' IDENTIFIED BY 'gizli_sifre';
GRANT SELECT, PROCESS, REPLICATION CLIENT ON *.* TO 'netdata_monitor'@'localhost';
FLUSH PRIVILEGES;
"

# Nginx log dosyasına okuma izni
usermod -aG adm netdata
# veya
setfacl -m u:netdata:r /var/log/nginx/access.log

Plugin davranışını kontrol etmek için netdata.conf dosyasına eklenebilecek ayarlar:

# /etc/netdata/netdata.conf

[plugin:mysql_connections]
    update every = 5
    command options =

[plugin:nginx_log_analyzer]
    update every = 1
    command options =

Plugin’leri debug modunda test etmek için:

# Plugin'i Netdata ortam değişkenleriyle test et
sudo -u netdata /usr/libexec/netdata/plugins.d/mysql_connections.plugin 1

# Python.d plugin'lerini test et
sudo -u netdata /usr/libexec/netdata/plugins.d/python.d.plugin redis_queue debug trace

Alarm Tanımlama: Özel Metriklere Alert Eklemek

Plugin’i yazdınız ve metrikler akıyor. Şimdi bu metrikler için alarm tanımlayalım. Netdata’nın alarm sistemi özel metriklerinizle de mükemmel çalışır:

# /etc/netdata/health.d/custom_metrics.conf

# MySQL bağlantıları için alarm
 alarm: mysql_connections_high
    on: mysql.connections
    lookup: average -2m unaligned of active
    units: connections
    every: 30s
    warn: $this > (($max_allowed) * 0.7)
    crit: $this > (($max_allowed) * 0.9)
    info: MySQL aktif bağlantı sayısı kritik seviyeye yaklaşıyor
    to: sysadmin

# Redis kuyruğu büyüyorsa uyar
 alarm: email_queue_backlog
    on: redis_queue.length
    lookup: average -5m unaligned of email_queue
    units: items
    every: 1m
    warn: $this > 1000
    crit: $this > 5000
    info: E-posta kuyruğu birikmeye başladı, işçi süreçleri kontrol edin
    to: sysadmin

# Nginx 5xx hatası oranı
 alarm: nginx_5xx_spike
    on: nginx.status_codes
    lookup: sum -1m unaligned of 5xx
    units: requests
    every: 30s
    warn: $this > 50
    crit: $this > 200
    info: Son 1 dakikada aşırı sayıda 5xx hatası
    to: sysadmin

Alarm konfigürasyonunu test edin:

# Konfigürasyonu doğrula
netdatacli reload-health

# Aktif alarmları listele
netdatacli alarms

Yaygın Hatalar ve Çözümleri

Production’da plugin yazarken birkaç tuzakla karşılaşabilirsiniz:

stdout’u flush etmeyi unutmak en sık yapılan hata. Python’da print() varsayılan olarak tampon kullanır, Netdata veriyi okuyamaz:

# Yanlış
print("BEGIN mymetric.chart")
print("SET value = 42")
print("END")

# Doğru
print("BEGIN mymetric.chart")
print("SET value = 42")
print("END")
sys.stdout.flush()

CHART komutunu her döngüde yeniden göndermeyin. Grafik tanımı sadece bir kez gönderilmeli:

# Yanlış - her döngüde CHART gönderiliyor
while true; do
    echo "CHART myapp.metric ..."
    echo "DIMENSION value ..."
    echo "BEGIN myapp.metric"
    echo "SET value = $(get_metric)"
    echo "END"
    sleep 1
done

# Doğru - CHART sadece başta bir kez
echo "CHART myapp.metric ..."
echo "DIMENSION value ..."

while true; do
    echo "BEGIN myapp.metric"
    echo "SET value = $(get_metric)"
    echo "END"
    sleep 1
done

Hata durumunda plugin’in çökmemesi gerekir. Netdata çöken plugin’i yeniden başlatır ama bu metriklerde boşluk oluşturur:

# Plugin log'larını kontrol et
journalctl -u netdata -f | grep "plugin"

# Netdata kendi log'u
tail -f /var/log/netdata/error.log

Gerçek Dünya Senaryosu: E-Ticaret Sitesi İzleme

Bir e-ticaret sitesi düşünün. Sunucu metrikleri tek başına yetmez; sipariş işleme hızı, ödeme başarı oranı, aktif checkout session sayısı gibi iş metriklerini de izlemek istersiniz.

#!/usr/bin/env bash
# /usr/libexec/netdata/plugins.d/ecommerce_metrics.plugin

NETDATA_UPDATE_EVERY=${1:-5}
DB_HOST="localhost"
DB_NAME="ecommerce"
DB_USER="netdata_reader"

send_charts() {
    echo "CHART ecommerce.orders '' 'Sipariş Metrikleri' 'orders/min' ecommerce '' line 2000 ${NETDATA_UPDATE_EVERY}"
    echo "DIMENSION completed 'Tamamlanan' incremental 1 1"
    echo "DIMENSION failed 'Başarısız' incremental 1 1"
    echo "DIMENSION pending 'Bekleyen' absolute 1 1"

    echo "CHART ecommerce.revenue '' 'Anlık Gelir' 'TL/min' ecommerce '' area 2001 ${NETDATA_UPDATE_EVERY}"
    echo "DIMENSION revenue 'Gelir' incremental 1 100"

    echo "CHART ecommerce.sessions '' 'Aktif Oturumlar' 'sessions' ecommerce '' line 2002 ${NETDATA_UPDATE_EVERY}"
    echo "DIMENSION active_checkouts 'Checkout Aşamasında' absolute 1 1"
    echo "DIMENSION active_carts 'Sepette Ürün Var' absolute 1 1"
}

send_charts

while true; do
    # Sipariş istatistikleri
    ORDERS=$(mysql -u"${DB_USER}" -h"${DB_HOST}" "${DB_NAME}" 
        -e "SELECT
            SUM(CASE WHEN status='completed' THEN 1 ELSE 0 END) as completed,
            SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) as failed,
            SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as pending,
            COALESCE(SUM(CASE WHEN status='completed' THEN total_amount ELSE 0 END), 0) as revenue
        FROM orders
        WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 MINUTE);" 
        --batch --skip-column-names 2>/dev/null | head -1)

    COMPLETED=$(echo "$ORDERS" | awk '{print $1}')
    FAILED=$(echo "$ORDERS" | awk '{print $2}')
    PENDING=$(echo "$ORDERS" | awk '{print $3}')
    REVENUE=$(echo "$ORDERS" | awk '{print $4}' | tr -d '.')

    echo "BEGIN ecommerce.orders"
    echo "SET completed = ${COMPLETED:-0}"
    echo "SET failed = ${FAILED:-0}"
    echo "SET pending = ${PENDING:-0}"
    echo "END"

    echo "BEGIN ecommerce.revenue"
    echo "SET revenue = ${REVENUE:-0}"
    echo "END"

    # Aktif session'lar
    CHECKOUTS=$(mysql -u"${DB_USER}" -h"${DB_HOST}" "${DB_NAME}" 
        -e "SELECT
            COUNT(CASE WHEN stage='checkout' THEN 1 END),
            COUNT(CASE WHEN cart_item_count > 0 THEN 1 END)
        FROM user_sessions WHERE last_activity > DATE_SUB(NOW(), INTERVAL 15 MINUTE);" 
        --batch --skip-column-names 2>/dev/null | head -1)

    ACTIVE_CHECKOUT=$(echo "$CHECKOUTS" | awk '{print $1}')
    ACTIVE_CARTS=$(echo "$CHECKOUTS" | awk '{print $2}')

    echo "BEGIN ecommerce.sessions"
    echo "SET active_checkouts = ${ACTIVE_CHECKOUT:-0}"
    echo "SET active_carts = ${ACTIVE_CARTS:-0}"
    echo "END"

    sleep ${NETDATA_UPDATE_EVERY}
done

Sonuç

Netdata’nın plugin sistemi, sunucu izlemeyi gerçekten özelleştirmenize olanak tanıyor. Bash ile başlayıp Python veya Go’ya geçebilir, uygulamanıza özgü metrikleri standart sunucu metrikleriyle aynı panelde görebilirsiniz. Alarm sistemiyle entegre ettiğinizde ise “CPU %90’ın üzerinde” gibi genel uyarıların ötesine geçip “ödeme başarı oranı düştü” gibi iş odaklı alarmlar alabilirsiniz.

Plugin yazarken dikkat edilmesi gereken kritik noktaları özetleyelim:

  • stdout’u düzenli flush edin, aksi takdirde Netdata veriyi okuyamaz
  • CHART tanımını sadece başlangıçta bir kez gönderin
  • Plugin’iniz çökmemeli, hataları gracefully handle etmeli
  • netdata kullanıcısının gerekli izinleri olduğundan emin olun
  • Production’a almadan önce manuel test edin

İlerleyen dönemde Netdata’nın Grafana ile entegrasyonu ya da Prometheus exporter yazmak gibi konulara geçmeden önce bu temeli sağlam atmanız önemli. Kendi plugin’lerinizi yazınca Netdata’nın ne kadar esnek bir araç olduğunu daha iyi anlayacaksınız.

Benzer Konular

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir