Ö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.
