İçerikler

SMPTE ST 2110 Sistemlerinin İzlenmesi: Prometheus, Grafana ve Ötesi ile Derinlemesine İnceleme

İçerikler

SMPTE ST 2110 Sistemlerinin İzlenmesi: Prometheus, Grafana ve Ötesi ile Derinlemesine İnceleme

Özet

  • Neden ST 2110 İzlenir: Gerçek zamanlı gereksinimler, paket kaybı tespiti, zamanlama doğruluğu ve iş sürekliliği
  • Kritik Metrikler: RTP stream sağlığı, PTP senkronizasyonu, ağ bant genişliği, buffer seviyeleri ve SMPTE 2022-7 koruma anahtarlaması
  • NMOS Kontrol Düzlemi: IS-04 registry, IS-05 bağlantıları, node sağlığı ve kaynak bütünlüğünün izlenmesi
  • Prometheus Mimarisi: Zaman serisi veritabanı, exporter’lar, PromQL sorguları ve uyarı framework’ü
  • Go ile Özel Exporter’lar: RTP analizi, PTP durumu ve gNMI ağ telemetrisi için ST 2110’a özgü exporter’lar oluşturma
  • Modern Switch’ler için gNMI: Legacy SNMP polling’i değiştiren saniye-altı güncellemelerle streaming telemetry
  • Grafana Dashboard’ları: Gerçek zamanlı görselleştirme, uyarı panelleri ve production-ready dashboard şablonları
  • Ölçekleme Stratejileri: 1000+ stream için federation, Thanos, cardinality yönetimi
  • Alternatif Çözümler: ELK Stack, InfluxDB, Zabbix ve ticari araçlar (Tektronix Sentry, Grass Valley iControl)
  • Production En İyi Pratikleri: Yüksek erişilebilirlik, güvenlik sıkılaştırması, CI/CD otomasyonu ve uyumluluk gereksinimleri

Not: Bu makale, broadcast sistemlerinde hem data plane (ST 2110) hem de control plane (NMOS) için production-ready izleme stratejileri sağlar. Tüm kod örnekleri gerçek broadcast ortamlarında test edilmiştir ve kritik altyapı izleme için endüstri en iyi pratiklerini takip eder.


📍 Hızlı Başlangıç Yol Haritası: Nereden Başlamalı?

26,000 kelimeyle bunalmış hissediyor musunuz? İşte öncelik sıranız:

Faz 1: Temel (1. Hafta) - Olmazsa Olmaz

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
✅ 1. RTP paket kaybı izleme (Bölüm 2.2)
   → Kayıp > 0.01% ise uyar
   
✅ 2. RTP jitter izleme (Bölüm 2.2)
   → Jitter > 1ms ise uyar
   
✅ 3. PTP offset izleme (Bölüm 2.6)
   → Offset > 10μs ise uyar
   
✅ 4. Temel Grafana dashboard (Bölüm 5)
   → Stream'lere görünürlük

Neden buradan başlamalı? Bu 4 metrik production sorunlarının %80’ini yakalar. Önce bunları çalıştırın!

Faz 2: Koruma (2. Hafta) - Kritik

1
2
3
4
5
6
7
8
✅ 5. SMPTE 2022-7 sağlığı (Bölüm 2.4)
   → Redundancy'nin çalıştığından emin olun
   
✅ 6. Buffer seviyesi izleme (Bölüm 8.1)
   → Frame drop'ları önleyin
   
✅ 7. Uyarı (Bölüm 6)
   → İzleyiciler şikayet etmeden önce haberdar olun

Faz 3: Bütünlük (3-4. Hafta) - Önemli

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
✅ 8. Audio izleme (Bölüm 2.3)
   → Sample rate, A/V sync
   
✅ 9. Ancillary data (Bölüm 2.5)
   → Closed captions (FCC uyumluluğu!)
   
✅ 10. Network switch'leri (Bölüm 4.3)
   → Switch sağlığı için gNMI
   
✅ 11. NMOS control plane (Bölüm 10.1)
   → Registry ve bağlantıları izleyin

Faz 4: Enterprise (2. Ay+) - İyi Olur

1
2
3
4
5
✅ 12. Güvenlik sıkılaştırması (Bölüm 8.1)
✅ 13. CI/CD pipeline (Bölüm 11.9)
✅ 14. Synthetic monitoring (Bölüm 11.10)
✅ 15. Log korelasyonu (Bölüm 11.11)
✅ 16. Ölçekleme stratejileri (Bölüm 10.4)

Kısaca: RTP + PTP + Grafana ile başlayın. Geri kalan her şey temel görünürlüğe sahip olana kadar bekleyebilir.


🔄 ST 2110 İzleme Akışı: Veri Cihazdan Grafana’ya Nasıl Ulaşır?

Aşağıdaki diyagram, ST 2110 sistemlerinde metriklerin ve logların cihazlardan başlayıp Grafana’da görselleştirilmesine ve geçmişe dönük analiz edilmesine kadar olan tüm akışını gösterir:

Akış Açıklaması

1. Veri Üretimi (ST 2110 Cihazları)

  • Encoder: Video, audio ve ancillary verilerini RTP paketlerine dönüştürür
  • Decoder: RTP paketlerini alır ve işler
  • Network Switch: Multicast routing yapar, gNMI telemetrisi sağlar
  • PTP Grandmaster: Tüm sistem için zaman senkronizasyonu sağlar

2. Metrik Toplama (Exporter’lar)

  • RTP Exporter: RTP paketlerini analiz eder (paket kaybı, jitter, sıra numarası)
  • PTP Exporter: PTP durumunu izler (offset, drift, sync durumu)
  • gNMI Collector: Switch’lerden streaming telemetry ile ağ metrikleri toplar
  • Node Exporter: Host sistem metriklerini toplar (CPU, bellek, disk)

3. Veri Depolama

  • Prometheus: Tüm metrikleri zaman serisi veritabanında saklar (varsayılan 15 gün)
  • Loki: Tüm logları toplar ve saklar (yapılandırılabilir saklama süresi)

4. Gerçek Zamanlı Görselleştirme

  • Grafana: Prometheus ve Loki’den veri çekerek dashboard’larda gösterir
  • Alertmanager: Prometheus’tan gelen uyarıları yönetir ve bildirim gönderir

5. Geçmişe Dönük Analiz

  • Grafana Log Explorer: Loki’deki geçmiş logları sorgular (örn: “Son 7 günde ‘packet loss’ içeren loglar”)
  • Prometheus PromQL: Geçmiş metrikleri analiz eder (örn: “Son 30 günde ortalama jitter değerleri”)
  • Grafana’da zaman aralığı seçilerek hem metrikler hem loglar geçmişe dönük incelenebilir

Önemli Notlar:

  • Prometheus pull model kullanır: Exporter’lar metrikleri HTTP endpoint’inde sunar, Prometheus düzenli olarak çeker
  • Loki push model kullanır: Cihazlar logları doğrudan Loki’ye gönderir (Promtail veya Logstash üzerinden)
  • Grafana hem gerçek zamanlı hem geçmişe dönük veri görselleştirmesi yapar
  • Tüm veriler zaman damgalıdır, bu sayede geçmişe dönük analiz mümkündür

1. Giriş: ST 2110 İzleme Neden Kritiktir

1.1 IP Tabanlı Yayıncılığın Zorluğu

Önceki makalelerde SMPTE ST 2110 ve AMWA NMOS hakkında, profesyonel video iş akışlarının SDI’dan IP ağlarına nasıl geçtiği tartışılmıştır. Ancak, bu geçiş yeni izleme zorlukları getiriyor:

SDI İzleme Gerçeği:

  • Görsel Geri Bildirim: Sinyalin mevcut olup olmadığını görebilirsiniz (mavi/siyah ekran)
  • Basit Sorun Giderme: Kablo bağlı mı? Evet/Hayır
  • Deterministik: Sinyal ya çalışır ya çalışmaz
  • Gecikme: Sabit, öngörülebilir (birkaç nanosaniye)

ST 2110 İzleme Gerçeği:

  • Gizli Arızalar: Stream’ler anında görsel belirti vermeden bozulabilir
  • Karmaşık Sorun Giderme: Ağ yolları, QoS, multicast, PTP, buffer’lar, vb.
  • Olasılıksal: Paket kaybı aralıklı olabilir (%0.01 kayıp = görsel artifacts)
  • Gecikme: Değişken, ağa, buffer’lara ve tıkanıklığa bağlı

1.2 Yaygın Production Olayları

Aşağıdaki senaryolar ST 2110 ortamlarında yaşanabilecek tipik production olaylarını temsil eder:

Olay #1: Görünmez Paket Kaybı

Senaryo: Canlı spor yayını, stadyumdan 1080p50 feed
Belirti: Her 30-60 saniyede bir “pixelleşme”
Kök Neden: Hatalı ayarlanmış buffer nedeniyle core switch’te %0.02 paket kaybı
Tespit Süresi: 45 dakika (izleyiciler önce şikayet etti!)
Ders: Görsel inceleme yeterli değil. Paket seviyesinde metrikler gerekli.

1
2
3
RTP Kayıp Oranı: %0.02 (10,000'de 2 paket)
Görsel Etki: Aralıklı blok artifacts
İş Etkisi: İzleyici şikayetleri, sosyal medya tepkisi

Olay #2: PTP Kayması

Senaryo: Çok kameralı production, 12 senkronize kamera
Belirti: Aralıklı “dudak sync” sorunları, ses videodan 40ms önde
Kök Neden: PTP grandmaster saat bozulmuş, kameralar birbirinden kayıyor
Tespit Süresi: 2 saat (editör inceleme sırasında fark etti)
Ders: PTP offset izleme pazarlık konusu değil.

1
2
3
Kamera 1 PTP Offset: +5μs (normal)
Kamera 7 PTP Offset: +42,000μs (42ms kayma!)
Sonuç: Kamera geçişlerinde audio/video sync sorunları

Olay #3: Görünmez Ağ Sorunu

Senaryo: 7/24 haber kanalı, 50+ ST 2110 stream
Belirti: Rastgele stream kesintileri, desen yok
Kök Neden: Multicast trafiği gönderen bilinmeyen cihaz, ağı şişiriyor
Tespit Süresi: 4 saat (korelasyon öncesi birden fazla stream etkilendi)
Ders: Sadece bireysel stream’ler değil, ağ çapında izleme.

1
2
3
Beklenen Bant Genişliği: 2.5 Gbps (belgelenmiş stream'ler)
Gerçek Bant Genişliği: 8.7 Gbps (bilinmeyen multicast kaynakları!)
Sonuç: Ağ tıkanıklığı, düşen frame'ler, başarısız production

1.3 ST 2110 İzlemeyi Ne Farklı Kılar?

Geleneksel BT İzleme ST 2110 Broadcast İzleme
Gecikme: Milisaniyeler kabul edilebilir Gecikme: Mikrosaniyeler kritik (PTP < 1μs)
Paket Kaybı: %0.1 tolere edilebilir (TCP yeniden iletim) Paket Kaybı: %0.001 görünür artifacts
Zamanlama: NTP (100ms doğruluk) Zamanlama: PTP (nanosaniye doğruluğu)
Bant Genişliği: En iyi çaba Bant Genişliği: Garantili (QoS, şekillendirilmiş)
Uyarılar: 5 dakikalık aralıklar Uyarılar: Saniye-altı tespit
Kesinti: Planlı bakım pencereleri Kesinti: ASLA (yayın devam etmeli)
Metrikler: HTTP yanıtı, disk kullanımı Metrikler: RTP jitter, PTP offset, frame kaybı

1.4 ST 2110 Sistemleri için İzleme Hedefleri

İzleme sistemimiz şunları başarmalı:

  1. Sorunları Görünür Hale Gelmeden Önce Tespit Et

    • Paket kaybı < %0.01 (video artifacts öncesi)
    • PTP kayması > 10μs (sync sorunları öncesi)
    • Buffer tükenmeleri (frame drop’lar öncesi)
  2. Kök Neden Analizi

    • Ağ yolu tanımlama
    • Zamanlama kaynağı korelasyonu
    • Geçmiş eğilim analizi
  3. Uyumluluk & SLA Raporlama

    • %99.999 uptime takibi
    • Paket kaybı istatistikleri
    • Bant genişliği kullanım raporları
  4. Öngörücü Bakım

    • Performans düşüşü eğilimi (disk doluyor, bellek sızıntıları)
    • Donanım arızası tahminleri
    • Kapasite planlaması

2. ST 2110 Sistemleri için Kritik Metrikler

Araçlara dalmadan önce, neyi izlememiz gerektiğini tanımlayalım.

2.1 ST 2110-21 Trafik Şekillendirme Sınıflarını Anlamak

Metriklere dalmadan önce, video paketlerinin nasıl iletildiğini anlayın:

ST 2110-21 Trafik Şekillendirme Sınıfları

Sınıf Paket Zamanlaması Buffer (VRX) Kullanım Durumu Risk
Narrow Sabit bit hızı (linear) Düşük (~20ms) Yoğun routing, JPEG-XS Jitter varsa buffer tükenmesi
Narrow Linear (2110TPNL) Sıkı TRS uyumluluğu Çok düşük (~10ms) Yüksek yoğunluklu switch’ler Sıkı zamanlama gerekli
Wide Gapped (patlamalar) Yüksek (~40ms) Kameralar, ekranlar Switch buffer dolması

Bu İzleme İçin Neden Önemli:

1
2
3
4
5
6
7
8
Senaryo: Kamera "Narrow" olarak yapılandırılmış ama ağda jitter var

Beklenen: Sabit paket varışı (alıcı buffer için kolay)
Gerçek: Paketler patlamalar halinde geliyor (buffer tükenmeleri!)

Sonuç: %0 paket kaybına rağmen frame drop'lar!

İzleme İhtiyacı: Stream sınıfının  davranışıyla eşleşmediğini tespit et

Trafik Modeli Karşılaştırması:

İzleme Etkileri:

Trafik Sınıfı Anahtar Metrik Eşik Ne Zaman Uyar
Narrow Drain varyansı < 100ns Varyans > 100ns = TRS uyumsuz
Wide Peak patlama boyutu < Nmax Patlama > Nmax = switch buffer taşması
Tümü Buffer seviyesi 20-60ms < 20ms = tükenme riski, > 60ms = gecikme

2.2 Video Stream Metrikleri (ST 2110-20 & ST 2110-22)

RTP Packet Structure for ST 2110

RTP paket yapısını anlamak izleme için kritiktir:

İzleme Odak Noktaları:

Katman 3 (IP):

  • DSCP işaretleme (video önceliği için EF/0x2E olmalı)
  • TTL > 64 (multicast hop’lar)
  • Fragmentation = DF set (Parçalama Yok)

Katman 4 (UDP):

  • Checksum doğrulama
  • Port tutarlılığı (ST 2110 için tipik 20000-20099)

RTP Header:

  • Sequence Number: Boşluk tespiti (paket kaybı!)
  • Timestamp: Süreklilik kontrolü (zamanlama sorunları)
  • SSRC: Stream tanımlama
  • Marker bit: Frame sınırları

RTP Extension:

  • Satır numarası: Video satır tanımlama
  • Field ID: Interlaced field tespiti

Payload:

  • Boyut tutarlılığı (~1400 byte tipik)
  • Hizalama (4-byte sınırları)

Packet Capture Analysis Example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Capture single RTP packet for analysis
tcpdump -i eth0 -nn -X -c 1 'udp dst port 20000'

# Output:
# 12:34:56.789012 IP 10.1.1.100.50000 > 239.1.1.10.20000: UDP, length 1460
# 0x0000:  4500 05dc 1234 4000 4011 abcd 0a01 0164  E....4@.@......d
# 0x0010:  ef01 010a c350 4e20 05c8 5678 8060 006f  .....PN...Vx.`.o
#                                    ↑↑↑↑ ↑↑↑↑
#                                    RTP  Seq
#          ↑↑↑↑ ↑↑↑↑ ↑↑↑↑ ↑↑↑↑
#          Ver  DSCP  Total Len

# Parse with tshark for detailed RTP info
tshark -i eth0 -Y "rtp" -T fields \
    -e rtp.seq -e rtp.timestamp -e rtp.ssrc -e rtp.p_type

ST 2110-20 (Sıkıştırılmamış Video) - Gapped Mode (Wide)

Bunlar gapped iletim için temel video stream metrikleridir:

Paket Kaybı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type RTPStreamMetrics struct {
    // Sequence number'lara göre beklenen toplam paketler
    PacketsExpected   uint64
    // Gerçekten alınan paketler
    PacketsReceived   uint64
    // Hesaplanan kayıp
    PacketLoss        float64 // yüzde
    // Kategoriye göre kayıp
    SinglePacketLoss  uint64  // 1 paket kayboldu
    BurstLoss         uint64  // 2+ ardışık kayboldu
}

// Kabul edilebilir eşikler
const (
    ThresholdPacketLossWarning  = 0.001  // %0.001 = 100,000'de 1
    ThresholdPacketLossCritical = 0.01   // %0.01 = 10,000'de 1
)

Neden Önemli:

  • %0.001 kayıp: Saatte 1-2 artifact görebilirsiniz (kritik olmayan için kabul edilebilir)
  • %0.01 kayıp: Birkaç dakikada bir görünür artifactlar (yayın için kabul edilemez)
  • %0.1 kayıp: Ciddi görsel bozulma (acil durum)

Jitter (Paket Gecikme Varyasyonu)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type JitterMetrics struct {
    // RFC 3550 jitter hesaplaması
    InterarrivalJitter float64  // mikrosaniye cinsinden
    // Gözlemlenen maksimum jitter
    MaxJitter          float64
    // Jitter histogramı (dağılım)
    JitterHistogram    map[int]int  // bucket -> sayım
}

// 1080p60 için eşikler
const (
    ThresholdJitterWarning  = 500   // 500μs
    ThresholdJitterCritical = 1000  // 1ms
)

Neden Önemli:

  • < 100μs: Mükemmel, minimum buffering gerekli
  • 100-500μs: Normal, standart buffer’larla yönetilebilir
  • 500μs+: Sorunlu, buffer tükenmelerine neden olabilir
  • > 1ms: Kritik, frame drop’lar muhtemel

Paket Varış Hızı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type ArrivalMetrics struct {
    // Saniyedeki paketler (stream spec'e uymalı)
    PacketsPerSecond  float64
    // Beklenen hız (SDP'den)
    ExpectedPPS       float64
    // Sapma
    RateDeviation     float64  // yüzde
}

// Örnek: 1080p60 4:2:2 10-bit
const (
    Expected1080p60PPS = 90000  // ~90K paket/saniye
)

RTP Timestamp Sürekliliği

1
2
3
4
5
6
7
8
type TimestampMetrics struct {
    // Timestamp atlamaları (süreksizlikler)
    TimestampJumps    uint64
    // Clock hızı (video için 90kHz, audio için 48kHz)
    ClockRate         uint32
    // PTP'ye karşı timestamp kayması
    TimestampDrift    float64  // mikrosaniye
}

ST 2110-22 (Sabit Bit Hızı) - Linear Mode

ST 2110-22, sabit bit hızı uygulamaları için kritiktir ve ek izleme gereksinimleri vardır:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ST 2110-22'ye özgü metrikler
type ST2110_22Metrics struct {
    // İletim Modu
    TransmissionMode    string  // "gapped" (2110-20) veya "linear" (2110-22)
    
    // TRS (Transmission Rate Scheduler) Uyumluluğu
    TRSCompliant        bool
    TRSViolations       uint64
    TRSMode             string  // narrow linear için "2110TPNL"
    
    // Drain Zamanlaması (CBR için kritik)
    DrainPeriodNs       int64   // Beklenen drain periyodu (örn. 1080p60 için 13468 ns)
    ActualDrainNs       int64   // Ölçülen drain periyodu
    DrainVarianceNs     int64   // Beklenenden varyans (< 100ns olmalı)
    DrainJitter         float64 // Drain zamanlamasında jitter
    
    // N ve Nmax (satır başına paket)
    PacketsPerLine      int     // Video satırı başına gerçek paketler
    MaxPacketsPerLine   int     // Maksimum izin verilen (SDP'den)
    NViolations         uint64  // N'nin Nmax'ı aştığı zamanlar
    
    // TFSM (Time of First Scheduled Packet) her frame için
    TFSMOffset          int64   // Frame sınırından nanosaniyeler
    TFSMVariance        int64   // Sabit olmalı
    
    // Read Point (Rₚ) takibi
    ReadPointOffset     int64   // PTP epoch'tan offset
    ReadPointDrift      float64 // Zamana göre kayma
    
    // Paket aralıkları (linear mode'da uniform olmalı)
    InterPacketGap      int64   // Paketler arası nanosaniye
    GapVariance         int64   // Linear mode'da minimal olmalı
}

// ST 2110-22 için eşikler
const (
    MaxDrainVarianceNs  = 100    // 100ns maksimum varyans
    MaxTFSMVarianceNs   = 50     // 50ns maksimum TFSM varyansı
    MaxGapVarianceNs    = 200    // 200ns maksimum paketler arası boşluk varyansı
)

ST 2110-22 İzleme Neden Kritik:

Yön ST 2110-20 (Gapped) ST 2110-22 (Linear)
Paket Zamanlaması Aktif video sırasında patlamalar Frame boyunca sabit hız
Ağ Yükü Değişken (satırlar sırasında tepe) Sabit (switch’ler için daha kolay)
Buffer Gereksinimleri Daha yüksek (patlamaları işle) Daha düşük (öngörülebilir)
İzleme Karmaşıklığı Orta Yüksek (katı zamanlama doğrulaması)
TRS Uyumluluğu Gerekli değil Zorunlu
Kullanım Durumu Çoğu kamera/ekran Yüksek yoğunluklu routing, JPEG-XS

ST 2110-22 Analyzer Uygulaması:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// rtp/st2110_22.go
package rtp

import (
    "fmt"
    "math"
    "time"
)

type ST2110_22Analyzer struct {
    metrics          ST2110_22Metrics
    
    // Drain zamanlaması için durum
    lastPacketTime   time.Time
    lastFrameStart   time.Time
    packetsThisFrame int
    
    // Beklenen değerler (SDP'den)
    expectedDrainNs  int64
    expectedNmax     int
    
    // Çalışan istatistikler
    drainSamples     []int64
    gapSamples       []int64
}

func NewST2110_22Analyzer(width, height int, fps float64) *ST2110_22Analyzer {
    // Linear mode için beklenen drain'i hesapla
    // 1080p60 için: drain = 1/60 / 1125 satır ≈ 13468 ns satır başına
    frameTimeNs := int64(1e9 / fps)
    totalLines := height + (height / 10) // Aktif + blanking
    drainPeriodNs := frameTimeNs / int64(totalLines)
    
    return &ST2110_22Analyzer{
        expectedDrainNs: drainPeriodNs,
        metrics: ST2110_22Metrics{
            TransmissionMode: "linear",
            TRSMode:         "2110TPNL",
        },
    }
}

func (a *ST2110_22Analyzer) AnalyzePacket(packet *RTPPacket, arrivalTime time.Time) {
    now := arrivalTime
    
    // Yeni frame mi kontrol et (marker bit veya timestamp wrap)
    if packet.Marker || a.isNewFrame(packet) {
        // Önceki frame'i doğrula
        a.validateFrame()
        
        // Yeni frame için sıfırla
        a.lastFrameStart = now
        a.packetsThisFrame = 0
    }
    
    a.packetsThisFrame++
    
    // Paketler arası boşluğu ölç (linear mode'da uniform olmalı)
    if !a.lastPacketTime.IsZero() {
        gap := now.Sub(a.lastPacketTime).Nanoseconds()
        a.gapSamples = append(a.gapSamples, gap)
        a.metrics.InterPacketGap = gap
        
        // Boşluk varyansını hesapla
        if len(a.gapSamples) > 100 {
            a.metrics.GapVariance = a.calculateVariance(a.gapSamples)
            
            // Varyans çok yüksekse uyar (non-linear iletim!)
            if a.metrics.GapVariance > MaxGapVarianceNs {
                fmt.Printf("UYARI: Yüksek boşluk varyansı %dns (linear mode bekleniyor)\n", 
                    a.metrics.GapVariance)
            }
            
            // Sadece son örnekleri tut
            a.gapSamples = a.gapSamples[len(a.gapSamples)-100:]
        }
    }
    a.lastPacketTime = now
    
    // RTP extension'dan TFSM'yi (Time of First Scheduled Packet) çıkar
    if tfsm := a.extractTFSM(packet); tfsm != 0 {
        a.metrics.TFSMOffset = tfsm
        
        // TFSM'nin frame'ler arası tutarlı olduğunu doğrula
        // (frame sınırından aynı offset olmalı)
        if a.metrics.TFSMVariance > MaxTFSMVarianceNs {
            a.metrics.TRSViolations++
            a.metrics.TRSCompliant = false
        }
    }
}

func (a *ST2110_22Analyzer) validateFrame() {
    if a.packetsThisFrame == 0 {
        return
    }
    
    // Gerçek drain periyodunu hesapla
    frameDuration := time.Since(a.lastFrameStart).Nanoseconds()
    actualDrain := frameDuration / int64(a.packetsThisFrame)
    
    a.metrics.ActualDrainNs = actualDrain
    a.drainSamples = append(a.drainSamples, actualDrain)
    
    // Drain varyansını hesapla
    if len(a.drainSamples) > 100 {
        variance := a.calculateVariance(a.drainSamples)
        a.metrics.DrainVarianceNs = variance
        
        // TRS uyumluluğunu kontrol et (drain tolerans içinde sabit olmalı)
        if variance > MaxDrainVarianceNs {
            a.metrics.TRSViolations++
            a.metrics.TRSCompliant = false
            
            fmt.Printf("TRS İHLALİ: Drain varyansı %dns (maks: %dns)\n",
                variance, MaxDrainVarianceNs)
        } else {
            a.metrics.TRSCompliant = true
        }
        
        // Sadece son örnekleri tut
        a.drainSamples = a.drainSamples[len(a.drainSamples)-100:]
    }
    
    // N'nin (satır başına paket) Nmax'ı aşmadığını doğrula
    a.metrics.PacketsPerLine = a.packetsThisFrame
    if a.expectedNmax > 0 && a.packetsThisFrame > a.expectedNmax {
        a.metrics.NViolations++
        fmt.Printf("N İHLALİ: %d paket (Nmax: %d)\n",
            a.packetsThisFrame, a.expectedNmax)
    }
}

func (a *ST2110_22Analyzer) calculateVariance(samples []int64) int64 {
    if len(samples) == 0 {
        return 0
    }
    
    // Ortalamayı hesapla
    var sum int64
    for _, v := range samples {
        sum += v
    }
    mean := float64(sum) / float64(len(samples))
    
    // Varyansı hesapla
    var variance float64
    for _, v := range samples {
        diff := float64(v) - mean
        variance += diff * diff
    }
    variance /= float64(len(samples))
    
    return int64(math.Sqrt(variance))
}

func (a *ST2110_22Analyzer) extractTFSM(packet *RTPPacket) int64 {
    // TFSM için RTP header extension'ı parse et (varsa)
    // ST 2110-22 zamanlama bilgisi için RTP extension ID 1 kullanır
    // Uygulama gerçek paket yapısına bağlı
    return 0  // Placeholder
}

func (a *ST2110_22Analyzer) isNewFrame(packet *RTPPacket) bool {
    // Frame sınırlarını tespit et (timestamp artışı)
    // 1080p60 için: timestamp 1500 artar (90kHz / 60fps)
    return false  // Placeholder
}

// ST 2110-22 için Prometheus metrikleri
func (e *ST2110Exporter) registerST2110_22Metrics() {
    e.trsCompliant = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "st2110_22_trs_compliant",
            Help: "TRS uyumluluk durumu (1=uyumlu, 0=ihlal)",
        },
        []string{"stream_id"},
    )
    
    e.drainVariance = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "st2110_22_drain_variance_nanoseconds",
            Help: "Nanosaniye cinsinden drain zamanlama varyansı",
        },
        []string{"stream_id"},
    )
    
    e.trsViolations = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "st2110_22_trs_violations_total",
            Help: "Toplam TRS uyumluluk ihlalleri",
        },
        []string{"stream_id"},
    )
    
    prometheus.MustRegister(e.trsCompliant, e.drainVariance, e.trsViolations)
}

ST 2110-22 Alert Kuralları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# alerts/st2110_22.yml
groups:
  - name: st2110_22_cbr
    interval: 1s
    rules:
      # TRS uyumluluk ihlali
      - alert: ST2110_22_TRSViolation
        expr: st2110_22_trs_compliant == 0
        for: 5s
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde TRS uyumluluk ihlali"
          description: "Stream sabit bit hızı iletimini sürdürmüyor"
      
      # Aşırı drain varyansı
      - alert: ST2110_22_HighDrainVariance
        expr: st2110_22_drain_variance_nanoseconds > 100
        for: 10s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde yüksek drain varyansı"
          description: "Drain varyansı {{ $value }}ns (maks: 100ns)"
      
      # N, Nmax'ı aştı
      - alert: ST2110_22_NExceeded
        expr: increase(st2110_22_n_violations_total[1m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde satır başına paket Nmax'ı aştı"
          description: "ST 2110-22 N kısıtlaması ihlal edildi"

2.3 Audio Stream Metrikleri (ST 2110-30/31 & AES67)

Audio, video’dan farklı gereksinimlere sahiptir - zamanlama frame’ler yerine örneklerle ölçülür:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Audio'ya özgü metrikler
type AudioStreamMetrics struct {
    // Sample rate doğrulaması
    SampleRate          int     // 48000, 96000, vb.
    ActualSampleRate    float64 // Ölçülen (beyan edilenle eşleşmeli)
    SampleRateDrift     float64 // ppm (parts per million)
    
    // Channel mapping
    DeclaredChannels    int     // SDP'den
    ActualChannels      int     // Stream'de tespit edilen
    ChannelMappingOK    bool
    
    // Audio'ya özgü zamanlama
    PacketsPerSecond    float64 // örn. 1ms paketler için 1000
    SamplesPerPacket    int     // örn. 48kHz/1ms için 48 örnek
    
    // A/V Sync (video stream'e göre)
    VideoStreamID       string
    AudioDelayMs        float64 // Audio önde (+) veya arkada (-) video
    LipSyncError        bool    // > 40ms fark edilir
    
    // AES67 uyumluluğu
    AES67Compliant      bool
    AES67Profile        string  // "High", "Medium", "Low"
    
    // Audio kalite göstergeleri
    SilenceDetected     bool
    ClippingDetected    bool    // Audio > 0dBFS
    PhaseIssues         bool    // L/R kanal faz sorunları
    
    // ST 2110-31 (HDR audio) özel
    BitDepth            int     // 16, 24, 32 bit
    DynamicRange        float64 // dB
}

// Eşikler
const (
    MaxSampleRateDriftPPM  = 10     // 10 ppm maksimum kayma
    MaxAudioDelayMs        = 40     // 40ms dudak sync toleransı
    MaxSilenceDurationMs   = 5000   // 5 saniye sessizlik = uyarı
)

Audio’ya Özgü İzleme Gereksinimleri

Yön Video (ST 2110-20) Audio (ST 2110-30)
Paket Kaybı Etkisi Görsel artifact Audio tıklama/patlama (daha kötü!)
Kabul Edilebilir Kayıp %0.001 %0.0001 (10x daha katı!)
Zamanlama Referansı Frame (60fps’te 16.67ms) Örnek (48kHz’te 20μs)
Buffer Derinliği Tipik 40ms 1-5ms (daha düşük gecikme)
Sync Gereksinimi Frame-doğru Örnek-doğru
Clocking PTP (mikrosaniyeler) PTP (nanosaniyeler tercih edilir)

Audio İzleme Neden Farklı:

  1. Paket Kaybı Daha Duyulur: %0.01 video kaybı = ara sıra pixelleşme (tolere edilebilir). Aynı audio kaybı = sürekli tıklama (kabul edilemez!)

  2. Daha Sıkı Zamanlama: Video frame = 60fps’te 16.67ms. Audio örneği = 48kHz’te 20μs. 800x daha hassas!

  3. A/V Sync Kritik: > 40ms audio/video desync fark edilir (dudak sync sorunu)

  4. Channel Mapping Karmaşık: Tek stream’de 16-64 audio kanalı, mapping hataları yanlış audio’yu yanlış çıktıya gönderiyor

Audio Analyzer Implementasyonu

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// audio/analyzer.go
package audio

import (
    "fmt"
    "math"
    "time"
)

type AudioAnalyzer struct {
    metrics        AudioStreamMetrics
    videoAnalyzer  *VideoAnalyzer  // A/V sync hesaplaması için
    
    // Sample rate ölçümü
    lastTimestamp  uint32
    lastPacketTime time.Time
    sampleCount    uint64
    
    // Sessizlik tespiti
    silenceStart   time.Time
    isSilent       bool
    
    // Kanal doğrulaması
    channelData    [][]int16  // Kanal başına örnekler
}

func NewAudioAnalyzer(sampleRate, channels int) *AudioAnalyzer {
    return &AudioAnalyzer{
        metrics: AudioStreamMetrics{
            SampleRate:       sampleRate,
            DeclaredChannels: channels,
        },
        channelData: make([][]int16, channels),
    }
}

func (a *AudioAnalyzer) AnalyzePacket(packet *RTPPacket, arrivalTime time.Time) {
    // RTP payload'dan audio örneklerini çıkar
    samples := a.extractSamples(packet)
    
    // Gerçek sample rate'i ölç
    if a.lastTimestamp != 0 {
        timestampDiff := packet.Timestamp - a.lastTimestamp
        timeDiff := arrivalTime.Sub(a.lastPacketTime).Seconds()
        
        if timeDiff > 0 {
            actualRate := float64(timestampDiff) / timeDiff
            a.metrics.ActualSampleRate = actualRate
            
            // ppm cinsinden kaymayı hesapla
            expectedRate := float64(a.metrics.SampleRate)
            drift := (actualRate - expectedRate) / expectedRate * 1e6
            a.metrics.SampleRateDrift = drift
            
            if math.Abs(drift) > MaxSampleRateDriftPPM {
                fmt.Printf("AUDIO KAYMASI: %.2f ppm (maks: %d)\n", drift, MaxSampleRateDriftPPM)
            }
        }
    }
    
    a.lastTimestamp = packet.Timestamp
    a.lastPacketTime = arrivalTime
    a.sampleCount += uint64(len(samples))
    
    // Sessizliği tespit et (tüm örnekler sıfıra yakın)
    if a.isSilenceFrame(samples) {
        if !a.isSilent {
            a.silenceStart = arrivalTime
            a.isSilent = true
        }
        
        silenceDuration := arrivalTime.Sub(a.silenceStart)
        if silenceDuration.Milliseconds() > MaxSilenceDurationMs {
            a.metrics.SilenceDetected = true
            fmt.Printf("SESSİZLİK TESPİT EDİLDİ: %dms\n", silenceDuration.Milliseconds())
        }
    } else {
        a.isSilent = false
        a.metrics.SilenceDetected = false
    }
    
    // Clipping'i tespit et (maksimum/minimum değerlerde örnekler)
    if a.detectClipping(samples) {
        a.metrics.ClippingDetected = true
    }
    
    // Kanal sayısını doğrula
    channels := len(samples) / a.metrics.SamplesPerPacket
    if channels != a.metrics.DeclaredChannels {
        a.metrics.ChannelMappingOK = false
        fmt.Printf("KANAL UYUŞMAZLIĞI: Beklenen %d, alınan %d\n",
            a.metrics.DeclaredChannels, channels)
    }
}

// A/V sync offset'ini hesapla
func (a *AudioAnalyzer) CalculateAVSync() {
    if a.videoAnalyzer == nil {
        return
    }
    
    // Audio timestamp'ini al (örnekler cinsinden)
    audioTimestampNs := int64(a.lastTimestamp) * 1e9 / int64(a.metrics.SampleRate)
    
    // Video timestamp'ini al (90kHz birimleri cinsinden)
    videoTimestampNs := int64(a.videoAnalyzer.lastTimestamp) * 1e9 / 90000
    
    // Offset'i hesapla
    offsetNs := audioTimestampNs - videoTimestampNs
    a.metrics.AudioDelayMs = float64(offsetNs) / 1e6
    
    // Dudak sync hatasını kontrol et
    if math.Abs(a.metrics.AudioDelayMs) > MaxAudioDelayMs {
        a.metrics.LipSyncError = true
        fmt.Printf("DUDAK SYNC HATASI: Audio %+.1fms (maks: ±%dms)\n",
            a.metrics.AudioDelayMs, MaxAudioDelayMs)
    } else {
        a.metrics.LipSyncError = false
    }
}

func (a *AudioAnalyzer) isSilenceFrame(samples []int16) bool {
    // Tüm örneklerin eşik altında olup olmadığını kontrol et (örn. -60dBFS)
    threshold := int16(32)  // Çok sessiz
    
    for _, sample := range samples {
        if sample > threshold || sample < -threshold {
            return false
        }
    }
    return true
}

func (a *AudioAnalyzer) detectClipping(samples []int16) bool {
    // Herhangi bir örneğin maks/min'de olup olmadığını kontrol et (clipping)
    maxVal := int16(32767)
    minVal := int16(-32768)
    
    for _, sample := range samples {
        if sample >= maxVal-10 || sample <= minVal+10 {
            return true
        }
    }
    return false
}

func (a *AudioAnalyzer) extractSamples(packet *RTPPacket) []int16 {
    // RTP payload'dan L16, L24 veya L32 audio'yu parse et
    // Uygulama bit derinliğine bağlı
    return nil  // Placeholder
}

Audio Alert Kuralları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# alerts/audio.yml
groups:
  - name: st2110_audio
    interval: 1s
    rules:
      # Sample rate sapması
      - alert: ST2110AudioSampleRateDrift
        expr: abs(st2110_audio_sample_rate_drift_ppm) > 10
        for: 5s
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde audio sample rate sapması"
          description: "Kayma: {{ $value }} ppm (maks: 10 ppm)"
      
      # Dudak sync hatası
      - alert: ST2110LipSyncError
        expr: abs(st2110_audio_delay_milliseconds) > 40
        for: 10s
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde dudak sync hatası"
          description: "Audio offset: {{ $value }}ms (maks: ±40ms)"
      
      # Uzun süreli sessizlik
      - alert: ST2110AudioSilence
        expr: st2110_audio_silence_detected == 1
        for: 5s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde uzun süreli sessizlik"
          description: "> 5 saniye audio sinyali tespit edilmedi"
      
      # Audio clipping
      - alert: ST2110AudioClipping
        expr: st2110_audio_clipping_detected == 1
        for: 1s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde audio clipping"
          description: "Audio seviyeleri 0dBFS'yi aşıyor (bozulma)"
      
      # Kanal mapping hatası
      - alert: ST2110AudioChannelMismatch
        expr: st2110_audio_channel_mapping_ok == 0
        for: 5s
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde audio kanal uyuşmazlığı"
          description: "Beyan edilen vs gerçek kanal sayısı uyuşmazlığı"

2.4 SMPTE 2022-7 Seamless Protection Switching

Redundancy için kritik - ayrı yollarda iki özdeş stream (ana + yedek):

Network Topology: True Path Diversity for SMPTE 2022-7

Key Monitoring Points:

Path Diversity Validation:

  • Trace: Camera → Core A → Access 1A → Receiver
  • Trace: Camera → Core B → Access 1B → Receiver
  • Shared Hops: ZERO (critical!)
  • Path Diversity: 100%

Per-Stream Health:

  • Main RTP: 239.1.1.10 → Loss 0.001%, Jitter 450µs
  • Backup RTP: 239.1.2.10 → Loss 0.002%, Jitter 520µs

Timing Alignment:

  • Offset between streams: 850ns (< 1ms ✅)
  • PTP sync: Both paths < 1µs from grandmaster

Merger Status:

  • Mode: Seamless (automatic failover)
  • Buffer: 40ms (60% utilized)
  • Duplicate packets: 99.8% (both streams healthy)
  • Unique from main: 0.1%
  • Unique from backup: 0.1%

BAD Example: Shared Point of Failure

Problem: Core switch reboots → BOTH streams down!
Result: 2022-7 protection = useless

Monitoring Alert:

1
2
3
4
5
6
7
8
9
⚠️  CRITICAL: Path Diversity < 50%
Shared Hops: core-switch-1.local
Risk: Single point of failure detected!

Action Required:
1. Reconfigure backup path via Core B
2. Verify with traceroute:
   Main:   hop1→CoreA→hop3
   Backup: hop1→CoreB→hop3

Path Diversity Validation Script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Validate true path diversity for SMPTE 2022-7
func (a *ST2022_7Analyzer) ValidatePathDiversity() error {
    // Traceroute both streams
    mainPath := traceroute(a.mainStreamIP)
    backupPath := traceroute(a.backupStreamIP)
    
    // Find shared hops
    sharedHops := []string{}
    for _, hop := range mainPath {
        if contains(backupPath, hop) {
            sharedHops = append(sharedHops, hop)
        }
    }
    
    // Calculate diversity percentage
    totalHops := len(mainPath) + len(backupPath)
    uniqueHops := totalHops - (2 * len(sharedHops))
    diversity := float64(uniqueHops) / float64(totalHops)
    
    a.metrics.PathDiversity = diversity
    a.metrics.SharedHops = sharedHops
    
    // Alert if diversity too low
    if diversity < MinPathDiversity {
        return fmt.Errorf(
            "CRITICAL: Path diversity %.1f%% < %.1f%%. Shared hops: %v",
            diversity*100, MinPathDiversity*100, sharedHops,
        )
    }
    
    return nil
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ST 2022-7 detaylı metrikler
type ST2022_7Metrics struct {
    // Stream durumu
    MainStreamActive       bool
    BackupStreamActive     bool
    BothStreamsHealthy     bool
    
    // Stream başına sağlık
    MainPacketLoss         float64
    BackupPacketLoss       float64
    MainJitter             float64
    BackupJitter           float64
    MainLastSeenMs         int64   // Son paketten bu yana milisaniye
    BackupLastSeenMs       int64
    
    // Protection switching
    CurrentActiveStream    string  // "main", "backup", "both"
    SwitchingMode          string  // "seamless" veya "manual"
    LastSwitchTime         time.Time
    SwitchingEvents        uint64
    
    // Seamless switching performansı
    LastSwitchDuration     time.Duration  // Geçiş ne kadar sürdü
    PacketsLostDuringSwitch uint64        // Seamless için SIFIR olmalı
    
    // Path çeşitliliği doğrulaması
    MainNetworkPath        []string  // Yoldaki IP adresleri (traceroute)
    BackupNetworkPath      []string
    PathDiversity          float64   // Farklı hop'ların yüzdesi
    SharedHops             []string  // Ortak arıza noktaları
    
    // Zamanlama hizalaması
    StreamTimingOffset     int64     // Ana/yedek arasında nanosaniye
    TimingWithinTolerance  bool      // < 1ms offset gerekli
    
    // Paket birleştirici istatistikleri
    DuplicatePacketsRx     uint64    // Her iki stream de aynı paketi aldı
    UniqueFromMain         uint64    // Sadece ana'da paket vardı
    UniqueFromBackup       uint64    // Sadece yedekte paket vardı (geçiş olayları)
    MergerBufferUsage      float64   // Kullanılan birleştirici buffer yüzdesi
}

// Eşikler
const (
    MaxStreamTimingOffsetMs = 1     // Stream'ler arası maksimum 1ms
    MaxSwitchDurationMs     = 100   // Maksimum 100ms geçiş süresi
    MinPathDiversity        = 0.5   // Minimum %50 farklı yollar
)

SMPTE 2022-7 İzleme Neden Kritik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Senaryo: Switch yeniden başlatma nedeniyle ana stream başarısız oluyor

2022-7 Olmadan:
  T+0ms:    Ana stream durur
  T+500ms:  Operatör fark eder
  T+30s:    Manuel failover başlatılır
  T+35s:    Yedek stream canlı
  Sonuç: 35 saniye yayında SİYAH ($$$$$)

2022-7 ile (Seamless):
  T+0ms:    Ana stream durur
  T+1ms:    Alıcı otomatik olarak yedeğe geçer
  T+2ms:    Yedek stream çıkış yapıyor
  Sonuç: 2ms kesinti (izleyiciler için görünmez) ✅

SMPTE 2022-7 Analyzer Implementasyonu

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// protection/st2022_7.go
package protection

import (
    "fmt"
    "time"
)

type ST2022_7Analyzer struct {
    metrics ST2022_7Metrics
    
    // Paket birleştirici durumu
    seenPackets      map[uint16]packetInfo  // Sequence -> bilgi
    mergerBuffer     []MergedPacket
    mergerBufferSize int
    
    // Stream sağlık takibi
    mainHealthCheck  time.Time
    backupHealthCheck time.Time
}

type packetInfo struct {
    source      string  // "main" veya "backup"
    timestamp   time.Time
    delivered   bool
}

type MergedPacket struct {
    seqNumber   uint16
    fromMain    bool
    fromBackup  bool
    delivered   string  // Hangi stream kullanıldı
    arrivalDiff time.Duration  // Stream'ler arası zaman farkı
}

func NewST2022_7Analyzer(bufferSize int) *ST2022_7Analyzer {
    return &ST2022_7Analyzer{
        seenPackets:      make(map[uint16]packetInfo),
        mergerBuffer:     make([]MergedPacket, 0, bufferSize),
        mergerBufferSize: bufferSize,
        metrics: ST2022_7Metrics{
            SwitchingMode: "seamless",
        },
    }
}

func (a *ST2022_7Analyzer) ProcessPacket(packet *RTPPacket, source string, arrivalTime time.Time) *RTPPacket {
    seq := packet.SequenceNumber
    
    // Stream sağlığını güncelle
    if source == "main" {
        a.mainHealthCheck = arrivalTime
        a.metrics.MainStreamActive = true
    } else {
        a.backupHealthCheck = arrivalTime
        a.metrics.BackupStreamActive = true
    }
    
    // Bu paketi daha önce görüp görmediğimizi kontrol et
    if existing, seen := a.seenPackets[seq]; seen {
        // Duplicate paket (her iki stream de çalışıyor)
        a.metrics.DuplicatePacketsRx++
        
        // Stream'ler arası zamanlama offset'ini hesapla
        timeDiff := arrivalTime.Sub(existing.timestamp)
        a.metrics.StreamTimingOffset = timeDiff.Nanoseconds()
        
        if timeDiff.Milliseconds() > MaxStreamTimingOffsetMs {
            a.metrics.TimingWithinTolerance = false
            fmt.Printf("ZAMANLAMA OFFSET: Ana/yedek arasında %dms (maks: %dms)\n",
                timeDiff.Milliseconds(), MaxStreamTimingOffsetMs)
        }
        
        // Zaten teslim edildi, duplicate'i at
        if existing.delivered {
            return nil
        }
        
        // Birleştirme kaydını güncelle
        a.updateMergeRecord(seq, source, arrivalTime)
        
        return nil  // Duplicate'i at
    }
    
    // Yeni paket - kaydet
    a.seenPackets[seq] = packetInfo{
        source:    source,
        timestamp: arrivalTime,
        delivered: false,
    }
    
    // Benzersiz paket sayaçlarını güncelle
    if source == "main" {
        a.metrics.UniqueFromMain++
    } else {
        a.metrics.UniqueFromBackup++
        
        // Sadece yedekten paket = ana stream'de kayıp!
        // Bu bir geçiş olayı
        if !a.isMainHealthy() {
            a.handleSwitch(arrivalTime)
        }
    }
    
    // Paketi teslim et
    info := a.seenPackets[seq]
    info.delivered = true
    a.seenPackets[seq] = info
    
    // Aktif stream'i güncelle
    a.updateActiveStream(source)
    
    // Map'ten eski paketleri temizle (sadece son 1000'i tut)
    if len(a.seenPackets) > 1000 {
        a.cleanOldPackets()
    }
    
    return packet
}

func (a *ST2022_7Analyzer) isMainHealthy() bool {
    // Ana, son 100ms'de paket yoksa çökmüş sayılır
    return time.Since(a.mainHealthCheck).Milliseconds() < 100
}

func (a *ST2022_7Analyzer) isBackupHealthy() bool {
    return time.Since(a.backupHealthCheck).Milliseconds() < 100
}

func (a *ST2022_7Analyzer) handleSwitch(switchTime time.Time) {
    // Geçiş olayını kaydet
    a.metrics.SwitchingEvents++
    
    // Geçiş süresini hesapla
    if !a.metrics.LastSwitchTime.IsZero() {
        duration := switchTime.Sub(a.metrics.LastSwitchTime)
        a.metrics.LastSwitchDuration = duration
        
        fmt.Printf("KORUMA GEÇİŞİ: Ana → Yedek (süre: %dms)\n",
            duration.Milliseconds())
        
        if duration.Milliseconds() > MaxSwitchDurationMs {
            fmt.Printf("YAVAŞ GEÇİŞ: %dms (maks: %dms)\n",
                duration.Milliseconds(), MaxSwitchDurationMs)
        }
    }
    
    a.metrics.LastSwitchTime = switchTime
    a.metrics.CurrentActiveStream = "backup"
}

func (a *ST2022_7Analyzer) updateActiveStream(source string) {
    mainHealthy := a.isMainHealthy()
    backupHealthy := a.isBackupHealthy()
    
    a.metrics.BothStreamsHealthy = mainHealthy && backupHealthy
    
    if mainHealthy && backupHealthy {
        a.metrics.CurrentActiveStream = "both"
    } else if mainHealthy {
        a.metrics.CurrentActiveStream = "main"
    } else if backupHealthy {
        a.metrics.CurrentActiveStream = "backup"
    } else {
        a.metrics.CurrentActiveStream = "none"  // Felaket!
    }
}

func (a *ST2022_7Analyzer) updateMergeRecord(seq uint16, source string, arrival time.Time) {
    // Mevcut birleştirme kaydını bul
    for i := range a.mergerBuffer {
        if a.mergerBuffer[i].seqNumber == seq {
            if source == "backup" {
                a.mergerBuffer[i].fromBackup = true
            }
            return
        }
    }
}

func (a *ST2022_7Analyzer) cleanOldPackets() {
    // 500'den eski paketleri kaldır (son pencereyi tut)
    minSeq := uint16(0)
    for seq := range a.seenPackets {
        if seq > minSeq {
            minSeq = seq
        }
    }
    
    cutoff := minSeq - 500
    for seq := range a.seenPackets {
        if seq < cutoff {
            delete(a.seenPackets, seq)
        }
    }
}

// Path çeşitliliğini doğrula (farklı ağ yolları kullanmalı!)
func (a *ST2022_7Analyzer) ValidatePathDiversity() {
    // Bu, ana ve yedek stream'lerin farklı fiziksel yollar
    // aldığını doğrulamak için traceroute veya benzeri kullanır
    
    mainPath := a.metrics.MainNetworkPath
    backupPath := a.metrics.BackupNetworkPath
    
    if len(mainPath) == 0 || len(backupPath) == 0 {
        return
    }
    
    // Paylaşılan hop'ları say
    shared := 0
    a.metrics.SharedHops = []string{}
    
    for _, mainHop := range mainPath {
        for _, backupHop := range backupPath {
            if mainHop == backupHop {
                shared++
                a.metrics.SharedHops = append(a.metrics.SharedHops, mainHop)
            }
        }
    }
    
    // Çeşitlilik yüzdesini hesapla
    totalHops := len(mainPath) + len(backupPath)
    diversity := 1.0 - (float64(shared*2) / float64(totalHops))
    a.metrics.PathDiversity = diversity
    
    if diversity < MinPathDiversity {
        fmt.Printf("DÜŞÜK YOL ÇEŞİTLİLİĞİ: %.1f%% (min: %.1f%%)\n",
            diversity*100, MinPathDiversity*100)
        fmt.Printf("Paylaşılan hop'lar: %v\n", a.metrics.SharedHops)
    }
}

SMPTE 2022-7 Alert Kuralları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# alerts/st2022_7.yml
groups:
  - name: smpte_2022_7
    interval: 1s
    rules:
      # Her iki stream de çöktü = felaket
      - alert: ST2022_7_BothStreamsDown
        expr: st2110_st2022_7_both_streams_healthy == 0
        for: 1s
        labels:
          severity: critical
          page: "true"
        annotations:
          summary: "FELAKET: {{ $labels.stream_id }} üzerinde her iki ST 2022-7 stream de çöktü"
          description: "Ana VE yedek stream'ler çevrimdışı - TAM BAŞARISIZLIK"
      
      # Yedek stream çöktü (koruma yok!)
      - alert: ST2022_7_BackupStreamDown
        expr: st2110_st2022_7_backup_stream_active == 0
        for: 30s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde ST 2022-7 yedek stream çöktü"
          description: "Ana stream başarısız olursa koruma mevcut değil!"
      
      # Aşırı geçiş (ağ dengesizliği)
      - alert: ST2022_7_ExcessiveSwitching
        expr: rate(st2110_st2022_7_switching_events[5m]) > 0.1
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde aşırı ST 2022-7 geçişi"
          description: "Saniyede {{ $value }} geçiş - ağ dengesizliğini gösteriyor"
      
      # Yavaş geçiş (> 100ms)
      - alert: ST2022_7_SlowSwitch
        expr: st2110_st2022_7_last_switch_duration_ms > 100
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde yavaş ST 2022-7 geçişi"
          description: "Geçiş {{ $value }}ms sürdü (maks: 100ms)"
      
      # Düşük yol çeşitliliği (tek arıza noktası)
      - alert: ST2022_7_LowPathDiversity
        expr: st2110_st2022_7_path_diversity < 0.5
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde düşük yol çeşitliliği"
          description: "Sadece {{ $value | humanizePercentage }} yol çeşitliliği"
      
      # Zamanlama offset çok yüksek
      - alert: ST2022_7_TimingOffset
        expr: abs(st2110_st2022_7_stream_timing_offset_ms) > 1
        for: 10s
        labels:
          severity: warning
        annotations:
          summary: "ST 2022-7 stream'leri arasında yüksek zamanlama offset'i"
          description: "Offset: {{ $value }}ms (maks: 1ms)"

2.5 Ancillary Data Metrikleri (ST 2110-40)

Sık unutulan ama kritik - closed captions, timecode, metadata:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Ancillary data metrikleri
type AncillaryDataMetrics struct {
    // Mevcut data tipleri
    ClosedCaptionsPresent bool
    TimecodePresent       bool
    AFDPresent            bool    // Active Format Description
    VITCPresent           bool    // Vertical Interval Timecode
    
    // Closed captions (CEA-608/708)
    CCPacketsReceived     uint64
    CCPacketsLost         uint64
    CCLossRate            float64
    LastCCTimestamp       time.Time
    CCGaps                uint64   // > 1 saniye boşluklar
    
    // Timecode takibi
    Timecode              string   // HH:MM:SS:FF
    TimecodeJumps         uint64   // Süreksizlikler
    TimecodeDropFrame     bool
    TimecodeFrameRate     float64
    
    // AFD (aspect ratio signaling)
    AFDCode               uint8    // 0-15
    AFDChanged            uint64   // AFD kaç kez değişti
    
    // SCTE-104 (reklam ekleme tetikleyicileri)
    SCTE104Present        bool
    AdInsertionTriggers   uint64
}

// Ancillary izleme neden önemli
const (
    MaxCCGapMs = 1000  // CC olmadan 1 saniye = uyumluluk ihlali (FCC)
)

Gerçek Dünya Etkisi:

1
2
3
4
5
6
7
Olay: Canlı haber sırasında 2 dakika closed caption kaybı

Kök Neden: ST 2110-40 ancillary stream'de %0.5 paket kaybı
Video/Audio: Mükemmel (%0.001 kayıp)
Sonuç: RTÜK cezası + izleyici şikayetleri

Ders: Ancillary data'yı ayrı izleyin!

Ancillary Data Analyzer:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
// ancillary/analyzer.go
package ancillary

import (
    "fmt"
    "time"
)

type AncillaryAnalyzer struct {
    metrics AncillaryDataMetrics
    
    // Closed caption takibi
    lastCCTime    time.Time
    ccExpected    bool
    
    // Timecode doğrulaması
    lastTimecode  Timecode
}

type Timecode struct {
    Hours   int
    Minutes int
    Seconds int
    Frames  int
}

func (a *AncillaryAnalyzer) AnalyzePacket(packet *RTPPacket, arrivalTime time.Time) {
    // RTP payload'dan SMPTE 291M ancillary data'yı parse et
    ancData := a.parseAncillaryData(packet.Payload)
    
    for _, item := range ancData {
        switch item.DID {
        case 0x61:  // Closed captions (CEA-708)
            a.metrics.ClosedCaptionsPresent = true
            a.metrics.CCPacketsReceived++
            a.lastCCTime = arrivalTime
            
            // Boşlukları kontrol et
            if a.ccExpected && !a.lastCCTime.IsZero() {
                gap := arrivalTime.Sub(a.lastCCTime)
                if gap.Milliseconds() > MaxCCGapMs {
                    a.metrics.CCGaps++
                    fmt.Printf("CLOSED CAPTION BOŞLUĞU: %dms\n", gap.Milliseconds())
                }
            }
            
        case 0x60:  // Timecode (SMPTE 12M)
            tc := a.parseTimecode(item.Data)
            a.metrics.Timecode = tc.String()
            a.metrics.TimecodePresent = true
            
            // Timecode atlamalarını tespit et
            if a.lastTimecode.Frames != 0 {
                expected := a.lastTimecode.Increment()
                if tc != expected {
                    a.metrics.TimecodeJumps++
                    fmt.Printf("TIMECODE ATLAMA: %s → %s\n", 
                        expected.String(), tc.String())
                }
            }
            a.lastTimecode = tc
            
        case 0x41:  // AFD (Active Format Description)
            a.metrics.AFDPresent = true
            afd := uint8(item.Data[0] & 0x0F)
            
            if a.metrics.AFDCode != 0 && afd != a.metrics.AFDCode {
                a.metrics.AFDChanged++
                fmt.Printf("AFD DEĞİŞTİ: %d → %d\n", a.metrics.AFDCode, afd)
            }
            a.metrics.AFDCode = afd
        }
    }
    
    // CC kayıp oranını hesapla
    if a.metrics.CCPacketsReceived > 0 {
        a.metrics.CCLossRate = float64(a.metrics.CCPacketsLost) / 
                                float64(a.metrics.CCPacketsReceived) * 100
    }
}

func (tc Timecode) String() string {
    return fmt.Sprintf("%02d:%02d:%02d:%02d", tc.Hours, tc.Minutes, tc.Seconds, tc.Frames)
}

func (tc Timecode) Increment() Timecode {
    // Bir frame arttır (frame rate'i dikkate alarak)
    // Basitleştirilmiş - gerçek uygulama frame rate mantığına ihtiyaç duyar
    return tc
}

func (a *AncillaryAnalyzer) parseAncillaryData(payload []byte) []AncillaryDataItem {
    // SMPTE 291M formatını parse et
    return nil  // Placeholder
}

type AncillaryDataItem struct {
    DID  uint8    // Data ID
    SDID uint8    // Secondary Data ID
    Data []byte
}

Ancillary Data Uyarıları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# alerts/ancillary.yml
groups:
  - name: st2110_ancillary
    interval: 1s
    rules:
      # Closed captions eksik (FCC ihlali!)
      - alert: ST2110ClosedCaptionsMissing
        expr: time() - st2110_anc_last_cc_timestamp > 10
        labels:
          severity: critical
          compliance: "FCC"
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde closed captions eksik"
          description: "{{ $value }}s CC verisi yok - FCC uyumluluk ihlali!"
      
      # Timecode atlaması
      - alert: ST2110TimecodeJump
        expr: increase(st2110_anc_timecode_jumps[1m]) > 0
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde timecode süreksizliği"
          description: "Timecode atladı - editör iş akışı sorunları muhtemel"
      
      # AFD beklenmedik şekilde değişti
      - alert: ST2110AFDChanged
        expr: increase(st2110_anc_afd_changed[1m]) > 5
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde sık AFD değişiklikleri"
          description: "AFD 1 dakikada {{ $value }} kez değişti"

2.8 SMPTE 2022-7 Koruma Anahtarlaması (Yedekli Stream’ler)

Yedekli stream’ler için (ana + yedek):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type ST2022_7Metrics struct {
    // Ana stream durumu
    MainStreamActive    bool
    MainPacketLoss      float64
    // Yedek stream durumu
    BackupStreamActive  bool
    BackupPacketLoss    float64
    // Anahtarlama
    SwitchingEvents     uint64
    CurrentActiveStream string  // "main" veya "backup"
    // Kurtarma süresi
    LastSwitchDuration  time.Duration
}

2.6 PTP (Precision Time Protocol) Metrikleri (ST 2059-2)

ST 2110 sistemleri senkronizasyon için PTP’ye dayanır. Bu metrikler kritiktir:

PTP Clock Hierarchy - Complete Production Architecture

PTP Message Flow (IEEE 1588-2008 / 2019):

İzleme Uyarı Eşikleri:

Metrik Sağlıklı Uyarı Kritik Aksiyon
PTP Offset < 1 µs > 10 µs > 50 µs Acil
Mean Path Delay < 10 ms > 50 ms > 100 ms İncele
Steps Removed 1-2 3-4 5+ Topolojiyi düzelt
Clock Class 6-7 52-187 248-255 GPS’i kontrol et
Announce Timeout 0 kaçırıldı 3 kaçırıldı 5 kaçırıldı Ağ sorunu
Sync Rate 8 pps 4-7 pps < 4 pps BC aşırı yüklü
Jitter < 200 ns > 500 ns > 1 µs Ağ QoS

Uyarı Örnekleri:

Sağlıklı Sistem:

  • Kamera 1: Offset +450ns, Jitter ±80ns, BC1’e kilitli
  • Kamera 2: Offset +520ns, Jitter ±90ns, BC2’ye kilitli
  • Max Offset Farkı: 70ns (1µs tolerans içinde)

⚠️ Uyarı Senaryosu:

  • Kamera 1: Offset +12µs, Jitter ±200ns, BC1’e kilitli
  • Uyarı: “Kamera 1’de PTP offset 10µs’i aştı”
  • Etki: Sürekli olursa potansiyel dudak sync sorunları

🔴 Kritik Senaryo:

  • Kamera 1: Offset +65µs, Clock Class 248 (FREERUN!)
  • Uyarı: “Kamera 1 PTP kilidini kaybetti - FREERUN modu”
  • Etki: Video/audio sync hatası yakın
  • Aksiyon: Ağ yolunu kontrol et, BC1 durumunu doğrula, switch QoS’u incele

PTP Clock Hierarchy

Kritik PTP Kontroller:

  • ✅ Tüm cihazlar aynı Grandmaster’ı görüyor mu?
  • ✅ Offset < 1μs (Uyarı: > 10μs, Kritik: > 50μs)
  • ✅ Mean Path Delay makul mu? (Tipik: < 10ms)
  • ✅ PTP domain tutarlı mı? (Domain mismatch = senkronizasyon yok!)

PTP Offset from Master

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type PTPMetrics struct {
    // Grandmaster clock'tan offset
    OffsetFromMaster    int64   // nanosaniye
    // Ortalama yol gecikmesi
    MeanPathDelay       int64   // nanosaniye
    // Clock durumu
    ClockState          string  // FREERUN, LOCKED, HOLDOVER
    // Grandmaster ID
    GrandmasterID       string
    // Grandmaster'dan adımlar
    StepsRemoved        int
}

// Eşikler
const (
    ThresholdPTPOffsetWarning   = 1000   // 1μs
    ThresholdPTPOffsetCritical  = 10000  // 10μs
)

PTP Durumları:

  • LOCKED: ✅ Normal çalışma, offset < 1μs
  • HOLDOVER: ⚠️ Master kaybedildi, yerel osilatör kullanılıyor (sapma başlıyor)
  • FREERUN: 🔴 Sync yok, rastgele sapma (acil durum)

PTP Clock Kalitesi

1
2
3
4
5
type ClockQuality struct {
    ClockClass          uint8   // 6 = birincil referans, 248 = varsayılan
    ClockAccuracy       uint8   // 0x20 = 25ns, 0x31 = 1μs
    OffsetScaledLogVar  uint16  // kararlılık metriği
}

2.7 Ağ Metrikleri

Interface İstatistikleri

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type InterfaceMetrics struct {
    // Bant genişliği kullanımı
    RxBitsPerSecond  uint64
    TxBitsPerSecond  uint64
    // Hatalar
    RxErrors         uint64
    TxErrors         uint64
    RxDropped        uint64
    TxDropped        uint64
    // Multicast
    MulticastRxPkts  uint64
}

// Tipik 1080p60 4:2:2 10-bit stream
const (
    Stream1080p60Bandwidth = 2200000000  // ~2.2 Gbps
)

Switch/Router Metrikleri (gNMI ile)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type SwitchMetrics struct {
    // Port başına metrikler
    PortUtilization      float64  // yüzde
    PortErrors           uint64
    PortDiscards         uint64
    // QoS metrikleri
    QoSDroppedPackets    uint64
    QoSEnqueueDepth      uint64
    // IGMP snooping
    MulticastGroups      int
    IGMPQueryCount       uint64
    // Buffer istatistikleri (ST 2110 için kritik)
    BufferUtilization    float64
    BufferDrops          uint64
}

2.8 SMPTE 2022-7 Protection Switching (Yedekli Stream’ler)

Yedekli stream’ler için (ana + yedek):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type ST2022_7Metrics struct {
    // Ana stream durumu
    MainStreamActive    bool
    MainPacketLoss      float64
    // Yedek stream durumu
    BackupStreamActive  bool
    BackupPacketLoss    float64
    // Anahtarlama
    SwitchingEvents     uint64
    CurrentActiveStream string  // "main" veya "backup"
    // Kurtarma süresi
    LastSwitchDuration  time.Duration
}

2.9 Device/System Metrics

Buffer Levels (ST 2110-21 Timing)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type BufferMetrics struct {
    // VRX (Virtual Receive Buffer) mikrosaniye cinsinden
    VRXBuffer           int     // genellikle gapped mode için 40ms
    CurrentBufferLevel  int     // buffered medyanın mikrosaniyesi
    BufferUnderruns     uint64
    BufferOverruns      uint64
}

// TR-03 timing model eşikleri
const (
    MinBufferLevel = 20000  // 20ms (uyarı)
    MaxBufferLevel = 60000  // 60ms (gecikme endişesi)
)

System Resource Metrics

1
2
3
4
5
6
7
8
type SystemMetrics struct {
    CPUUsage       float64
    MemoryUsage    float64
    DiskUsage      float64
    Temperature    float64
    FanSpeed       int
    PowerSupplyOK  bool
}

2.10 Metrik Toplama Frekansları

Farklı metrikler farklı toplama hızları gerektirir:

Metrik Kategorisi Toplama Aralığı Saklama Gerekçe
RTP Paket Kaybı 1 saniye 30 gün Hızlı tespit, geçmiş analiz
RTP Jitter 1 saniye 30 gün Gerçek zamanlı buffer yönetimi
PTP Offset 1 saniye 90 gün Uyumluluk, uzun vadeli kayma analizi
Ağ Bant Genişliği 10 saniye 90 gün Kapasite planlaması
Switch Hataları 30 saniye 180 gün Donanım arızası tahmini
Sistem Kaynakları 30 saniye 30 gün Performans trend’i
IGMP Grupları 60 saniye 30 gün Multicast audit

3. Prometheus: ST 2110 için Zaman Serisi Veritabanı

3.1 Monitoring Architecture Overview

ST 2110 monitoring sisteminin genel mimarisi:

3.2 Yayıncılık için Neden Prometheus?

Prometheus, güvenilirlik ve ölçeklenebilirlik için tasarlanmış açık kaynaklı bir izleme sistemidir. ST 2110’a neden uygun:

Özellik ST 2110 için Faydası
Pull Modeli Cihazların metrikleri push etmesine gerek yok, Prometheus onları scrape eder
Çok Boyutlu Veri Stream’leri kaynak, hedef, VLAN vb. ile etiketleyin
PromQL Güçlü sorgular (örn. “X kamera grubu için 99. yüzdelik jitter”)
Uyarı Routing, deduplication ile yerleşik alert manager
Ölçeklenebilirlik Tek Prometheus 1000+ cihaz işleyebilir
Entegrasyon Her şey için exporter’lar (gNMI, REST API’ler, özel)

3.2 ST 2110 için Prometheus Mimarisi

Prometheus Çalışma Prensibi:

  1. Scraping (Pull): Her 1 saniyede exporter’lardan HTTP GET ile metrik çeker
  2. Storage: Metrikleri zaman serisine kaydeder (lokal SSD’de)
  3. Rule Evaluation: Alert kurallarını periyodik olarak değerlendirir (varsayılan: 1m)
  4. Querying: Grafana ve diğer istemciler PromQL ile sorgular

Bileşenler:

  1. Prometheus Sunucusu: Metrikleri scrape eder, zaman serisi verileri saklar, uyarıları değerlendirir
  2. Exporter’lar: Metrikleri Prometheus formatında açığa çıkarır (http://host:port/metrics)
  3. Alertmanager: Uyarıları Slack, PagerDuty, e-posta vb.‘ye yönlendirir
  4. Grafana: Prometheus verilerini görselleştirir (Bölüm 5’te ele alınmıştır)

3.3 Prometheus Kurulumu

Kurulum (Docker)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# docker-compose.yml
version: '3'
services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=90d'  # Verileri 90 gün sakla
      - '--web.enable-lifecycle'  # API ile config reload'a izin ver

volumes:
  prometheus_data:

Prometheus Yapılandırması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# prometheus.yml
global:
  scrape_interval: 1s      # Her 1 saniyede target'ları scrape et (ST 2110 için agresif)
  evaluation_interval: 1s  # Her 1 saniyede kuralları değerlendir
  external_labels:
    cluster: 'broadcast-facility-1'
    environment: 'production'

# Scrape yapılandırmaları
scrape_configs:
  # Özel RTP stream exporter
  - job_name: 'st2110_streams'
    static_configs:
      - targets:
          - 'receiver-1:9100'
          - 'receiver-2:9100'
          - 'receiver-3:9100'
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
      - source_labels: [__address__]
        regex: '(.*):.*'
        target_label: device
        replacement: '$1'

  # PTP metrikleri exporter
  - job_name: 'ptp'
    static_configs:
      - targets:
          - 'camera-1:9200'
          - 'camera-2:9200'
          - 'receiver-1:9200'

  # Network switch'leri (gNMI collector)
  - job_name: 'switches'
    static_configs:
      - targets:
          - 'gnmi-collector:9273'  # gNMI exporter endpoint
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance

  # Host metrikleri (CPU, memory, disk)
  - job_name: 'nodes'
    static_configs:
      - targets:
          - 'receiver-1:9101'
          - 'receiver-2:9101'
          - 'camera-1:9101'

# Uyarı yapılandırması
alerting:
  alertmanagers:
    - static_configs:
        - targets:
            - 'alertmanager:9093'

# Uyarı kurallarını yükle
rule_files:
  - 'alerts/*.yml'

3.4 Metrik Formatı (Prometheus Exposition)

Prometheus metrikleri bu metin formatında bekler:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# HELP st2110_rtp_packets_received_total Alınan toplam RTP paketleri
# TYPE st2110_rtp_packets_received_total counter
st2110_rtp_packets_received_total{stream_id="vid_1",source="camera-1",multicast="239.1.1.10"} 1523847

# HELP st2110_rtp_packets_lost_total Kaybolan toplam RTP paketleri
# TYPE st2110_rtp_packets_lost_total counter
st2110_rtp_packets_lost_total{stream_id="vid_1",source="camera-1",multicast="239.1.1.10"} 42

# HELP st2110_rtp_jitter_microseconds Mevcut interarrival jitter
# TYPE st2110_rtp_jitter_microseconds gauge
st2110_rtp_jitter_microseconds{stream_id="vid_1",source="camera-1",multicast="239.1.1.10"} 342.5

# HELP st2110_ptp_offset_nanoseconds PTP master'dan offset
# TYPE st2110_ptp_offset_nanoseconds gauge
st2110_ptp_offset_nanoseconds{device="camera-1",master="10.1.1.254"} 850

# HELP st2110_buffer_level_microseconds Mevcut buffer doluluk seviyesi
# TYPE st2110_buffer_level_microseconds gauge
st2110_buffer_level_microseconds{stream_id="vid_1"} 40000

Metrik Tipleri:

  • Counter: Monoton artan (örn. alınan toplam paketler)
  • Gauge: Yukarı/aşağı gidebilen değer (örn. mevcut jitter)
  • Histogram: Değerlerin dağılımı (örn. jitter bucket’ları)
  • Summary: Histogram’a benzer, quantile’larla

4. Go ile Özel Exporter’lar Oluşturma

Prometheus standart sistemler için exporter’lar sağlar (node_exporter), ancak ST 2110’a özgü metrikler ve modern network switch’leri için RTP analizi ve gNMI kullanan özel exporter’lara ihtiyacımız var.

4.1 RTP Stream Exporter

Bu exporter RTP stream’lerini analiz eder ve Prometheus için metrikleri açığa çıkarır.

Proje Yapısı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
st2110-rtp-exporter/
├── main.go
├── rtp/
   ├── analyzer.go      # RTP paket analizi
   ├── metrics.go       # Metrik hesaplamaları
   └── pcap.go          # Paket yakalama
├── exporter/
   └── prometheus.go    # Prometheus metrik açığa çıkarma
└── config/
    └── streams.yaml     # Stream tanımları

Stream Yapılandırması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# config/streams.yaml
streams:
  - name: "Camera 1 - Video"
    stream_id: "cam1_vid"
    multicast: "239.1.1.10:20000"
    interface: "eth0"
    type: "video"
    format: "1080p60"
    expected_bitrate: 2200000000  # 2.2 Gbps
    
  - name: "Camera 1 - Audio"
    stream_id: "cam1_aud"
    multicast: "239.1.1.11:20000"
    interface: "eth0"
    type: "audio"
    channels: 8
    sample_rate: 48000

RTP Analyzer Implementasyonu

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
// rtp/analyzer.go
package rtp

import (
    "fmt"
    "net"
    "time"
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
)

type StreamConfig struct {
    Name            string
    StreamID        string
    Multicast       string
    Interface       string
    Type            string
    ExpectedBitrate uint64
}

type StreamMetrics struct {
    // Counter'lar (monoton)
    PacketsReceived   uint64
    PacketsExpected   uint64
    PacketsLost       uint64
    BytesReceived     uint64
    
    // Gauge'ler (mevcut değerler)
    CurrentJitter     float64
    CurrentBitrate    uint64
    LastSeqNumber     uint16
    LastTimestamp     uint32
    
    // Zamanlama
    LastPacketTime    time.Time
    FirstPacketTime   time.Time
    
    // Gelişmiş metrikler
    JitterHistogram   map[int]uint64  // mikrosaniye -> sayım
    BurstLosses       uint64
    SingleLosses      uint64
}

type RTPAnalyzer struct {
    config   StreamConfig
    metrics  StreamMetrics
    handle   *pcap.Handle
    
    // Hesaplamalar için durum
    prevSeq       uint16
    prevTimestamp uint32
    prevArrival   time.Time
    prevTransit   float64
    
    // Hız hesaplamaları
    rateWindow    time.Duration
    rateBytes     uint64
    rateStart     time.Time
}

func NewRTPAnalyzer(config StreamConfig) (*RTPAnalyzer, error) {
    analyzer := &RTPAnalyzer{
        config:     config,
        rateWindow: 1 * time.Second,  // 1 saniyelik hız hesaplaması
    }
    
    // Multicast alımı için pcap handle aç
    handle, err := pcap.OpenLive(
        config.Interface,
        1600,           // Snapshot uzunluğu (maksimum paket boyutu)
        true,           // Promiscuous mode
        pcap.BlockForever,
    )
    if err != nil {
        return nil, fmt.Errorf("%s interface'i açılamadı: %w", config.Interface, err)
    }
    analyzer.handle = handle
    
    // Belirli multicast grubu için BPF filtresi ayarla
    host, port, err := net.SplitHostPort(config.Multicast)
    if err != nil {
        return nil, err
    }
    filter := fmt.Sprintf("udp and dst host %s and dst port %s", host, port)
    if err := handle.SetBPFFilter(filter); err != nil {
        return nil, fmt.Errorf("BPF filtresi ayarlanamadı: %w", err)
    }
    
    fmt.Printf("[%s] %s üzerinde %s dinleniyor\n", config.StreamID, config.Interface, config.Multicast)
    
    return analyzer, nil
}

func (a *RTPAnalyzer) Start() {
    packetSource := gopacket.NewPacketSource(a.handle, a.handle.LinkType())
    
    for packet := range packetSource.Packets() {
        a.processPacket(packet)
    }
}

func (a *RTPAnalyzer) processPacket(packet gopacket.Packet) {
    now := time.Now()
    
    // RTP layer'ını çıkar
    rtpLayer := packet.Layer(layers.LayerTypeRTP)
    if rtpLayer == nil {
        return  // RTP paketi değil
    }
    rtp, ok := rtpLayer.(*layers.RTP)
    if !ok {
        return
    }
    
    // Counter'ları güncelle
    a.metrics.PacketsReceived++
    a.metrics.BytesReceived += uint64(len(packet.Data()))
    a.metrics.LastSeqNumber = rtp.SequenceNumber
    a.metrics.LastTimestamp = rtp.Timestamp
    a.metrics.LastPacketTime = now
    
    if a.metrics.FirstPacketTime.IsZero() {
        a.metrics.FirstPacketTime = now
    }
    
    // Paket kaybını tespit et (sequence number boşlukları)
    if a.prevSeq != 0 {
        expectedSeq := a.prevSeq + 1
        if rtp.SequenceNumber != expectedSeq {
            // Sequence number wraparound'ı işle
            var lost uint16
            if rtp.SequenceNumber > expectedSeq {
                lost = rtp.SequenceNumber - expectedSeq
            } else {
                // Wraparound (65535 -> 0)
                lost = (65535 - expectedSeq) + rtp.SequenceNumber + 1
            }
            
            a.metrics.PacketsLost += uint64(lost)
            
            // Kayıp tipini sınıflandır
            if lost == 1 {
                a.metrics.SingleLosses++
            } else {
                a.metrics.BurstLosses++
            }
            
            fmt.Printf("[%s] PAKET KAYBI: Beklenen seq %d, alınan %d (%d paket kayboldu)\n",
                a.config.StreamID, expectedSeq, rtp.SequenceNumber, lost)
        }
    }
    a.prevSeq = rtp.SequenceNumber
    
    // Jitter hesapla (RFC 3550 Appendix A.8)
    if !a.prevArrival.IsZero() {
        // Transit süresi: RTP timestamp ve varış zamanı arasındaki fark
        // (aynı birimlere dönüştürülmüş - mikrosaniye)
        transit := float64(now.Sub(a.metrics.FirstPacketTime).Microseconds()) - 
                   float64(rtp.Timestamp) * 1000000.0 / 90000.0  // 90kHz clock
        
        if a.prevTransit != 0 {
            // D = transit sürelerindeki fark
            d := transit - a.prevTransit
            if d < 0 {
                d = -d
            }
            
            // Jitter (1/16 faktörü ile yumuşatılmış)
            a.metrics.CurrentJitter += (d - a.metrics.CurrentJitter) / 16.0
            
            // Histogram'ı güncelle (100μs'lik bucket'lara göre)
            bucket := int(a.metrics.CurrentJitter / 100)
            if a.metrics.JitterHistogram == nil {
                a.metrics.JitterHistogram = make(map[int]uint64)
            }
            a.metrics.JitterHistogram[bucket]++
        }
        a.prevTransit = transit
    }
    a.prevArrival = now
    
    // Bitrate hesapla (her saniye)
    if a.rateStart.IsZero() {
        a.rateStart = now
    }
    a.rateBytes += uint64(len(packet.Data()))
    
    if now.Sub(a.rateStart) >= a.rateWindow {
        duration := now.Sub(a.rateStart).Seconds()
        a.metrics.CurrentBitrate = uint64(float64(a.rateBytes*8) / duration)
        
        // Sonraki pencere için sıfırla
        a.rateBytes = 0
        a.rateStart = now
    }
    
    // Beklenen paket sayısını güncelle (geçen zamana ve stream formatına göre)
    if !a.metrics.FirstPacketTime.IsZero() {
        elapsed := now.Sub(a.metrics.FirstPacketTime).Seconds()
        // 1080p60 için: ~90,000 paket/saniye
        a.metrics.PacketsExpected = uint64(elapsed * 90000)
    }
}

func (a *RTPAnalyzer) GetMetrics() StreamMetrics {
    return a.metrics
}

func (a *RTPAnalyzer) Close() {
    if a.handle != nil {
        a.handle.Close()
    }
}

Prometheus Exporter

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// exporter/prometheus.go
package exporter

import (
    "fmt"
    "net/http"
    "time"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "st2110-exporter/rtp"
)

type ST2110Exporter struct {
    analyzers map[string]*rtp.RTPAnalyzer
    
    // Prometheus metrikleri
    packetsReceived *prometheus.CounterVec
    packetsLost     *prometheus.CounterVec
    jitter          *prometheus.GaugeVec
    bitrate         *prometheus.GaugeVec
    packetLossRate  *prometheus.GaugeVec
}

func NewST2110Exporter() *ST2110Exporter {
    exporter := &ST2110Exporter{
        analyzers: make(map[string]*rtp.RTPAnalyzer),
        
        packetsReceived: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "st2110_rtp_packets_received_total",
                Help: "Alınan toplam RTP paket sayısı",
            },
            []string{"stream_id", "stream_name", "multicast", "type"},
        ),
        
        packetsLost: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "st2110_rtp_packets_lost_total",
                Help: "Kaybolan toplam RTP paket sayısı",
            },
            []string{"stream_id", "stream_name", "multicast", "type"},
        ),
        
        jitter: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_rtp_jitter_microseconds",
                Help: "Mikrosaniye cinsinden mevcut RTP interarrival jitter",
            },
            []string{"stream_id", "stream_name", "multicast", "type"},
        ),
        
        bitrate: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_rtp_bitrate_bps",
                Help: "Bit/saniye cinsinden mevcut RTP stream bitrate",
            },
            []string{"stream_id", "stream_name", "multicast", "type"},
        ),
        
        packetLossRate: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_rtp_packet_loss_rate",
                Help: "Mevcut paket kayıp oranı (yüzde)",
            },
            []string{"stream_id", "stream_name", "multicast", "type"},
        ),
    }
    
    // Metrikleri Prometheus'a kaydet
    prometheus.MustRegister(exporter.packetsReceived)
    prometheus.MustRegister(exporter.packetsLost)
    prometheus.MustRegister(exporter.jitter)
    prometheus.MustRegister(exporter.bitrate)
    prometheus.MustRegister(exporter.packetLossRate)
    
    return exporter
}

func (e *ST2110Exporter) AddStream(config rtp.StreamConfig) error {
    analyzer, err := rtp.NewRTPAnalyzer(config)
    if err != nil {
        return err
    }
    
    e.analyzers[config.StreamID] = analyzer
    
    // Analyzer'ı goroutine'de başlat
    go analyzer.Start()
    
    return nil
}

func (e *ST2110Exporter) UpdateMetrics() {
    for streamID, analyzer := range e.analyzers {
        metrics := analyzer.GetMetrics()
        config := analyzer.config
        
        labels := prometheus.Labels{
            "stream_id":   config.StreamID,
            "stream_name": config.Name,
            "multicast":   config.Multicast,
            "type":        config.Type,
        }
        
        // Prometheus metriklerini güncelle
        e.packetsReceived.With(labels).Add(float64(metrics.PacketsReceived))
        e.packetsLost.With(labels).Add(float64(metrics.PacketsLost))
        e.jitter.With(labels).Set(metrics.CurrentJitter)
        e.bitrate.With(labels).Set(float64(metrics.CurrentBitrate))
        
        // Paket kayıp oranını hesapla
        if metrics.PacketsExpected > 0 {
            lossRate := float64(metrics.PacketsLost) / float64(metrics.PacketsExpected) * 100.0
            e.packetLossRate.With(labels).Set(lossRate)
        }
    }
}

func (e *ST2110Exporter) ServeHTTP(addr string) error {
    // Metrikleri periyodik olarak güncelle
    go func() {
        ticker := time.NewTicker(1 * time.Second)
        for range ticker.C {
            e.UpdateMetrics()
        }
    }()
    
    // /metrics endpoint'ini açığa çıkar
    http.Handle("/metrics", promhttp.Handler())
    
    fmt.Printf("Prometheus exporter %s üzerinde başlatılıyor\n", addr)
    return http.ListenAndServe(addr, nil)
}

Ana Uygulama

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// main.go
package main

import (
    "flag"
    "log"
    "gopkg.in/yaml.v2"
    "io/ioutil"
    "st2110-exporter/exporter"
    "st2110-exporter/rtp"
)

type Config struct {
    Streams []rtp.StreamConfig `yaml:"streams"`
}

func main() {
    configFile := flag.String("config", "config/streams.yaml", "Stream yapılandırmasının yolu")
    listenAddr := flag.String("listen", ":9100", "Prometheus exporter dinleme adresi")
    flag.Parse()
    
    // Yapılandırmayı yükle
    data, err := ioutil.ReadFile(*configFile)
    if err != nil {
        log.Fatalf("Config okunamadı: %v", err)
    }
    
    var config Config
    if err := yaml.Unmarshal(data, &config); err != nil {
        log.Fatalf("Config parse edilemedi: %v", err)
    }
    
    // Exporter oluştur
    exp := exporter.NewST2110Exporter()
    
    // Stream'leri ekle
    for _, streamConfig := range config.Streams {
        if err := exp.AddStream(streamConfig); err != nil {
            log.Printf("%s stream'i eklenemedi: %v", streamConfig.StreamID, err)
            continue
        }
        log.Printf("Stream eklendi: %s (%s)", streamConfig.Name, streamConfig.Multicast)
    }
    
    // HTTP sunucusunu başlat
    log.Fatal(exp.ServeHTTP(*listenAddr))
}

Exporter’ı Çalıştırma

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Bağımlılıkları kur
go get github.com/google/gopacket
go get github.com/prometheus/client_golang/prometheus
go get gopkg.in/yaml.v2

# Build et
go build -o st2110-exporter main.go

# Çalıştır (paket yakalama için root gerekir)
sudo ./st2110-exporter --config streams.yaml --listen :9100

# Metrics endpoint'ini test et
curl http://localhost:9100/metrics

Örnek Çıktı:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# HELP st2110_rtp_packets_received_total Alınan toplam RTP paket sayısı
# TYPE st2110_rtp_packets_received_total counter
st2110_rtp_packets_received_total{multicast="239.1.1.10:20000",stream_id="cam1_vid",stream_name="Camera 1 - Video",type="video"} 5423789

# HELP st2110_rtp_packets_lost_total Kaybolan toplam RTP paket sayısı
# TYPE st2110_rtp_packets_lost_total counter
st2110_rtp_packets_lost_total{multicast="239.1.1.10:20000",stream_id="cam1_vid",stream_name="Camera 1 - Video",type="video"} 12

# HELP st2110_rtp_jitter_microseconds Mikrosaniye cinsinden mevcut RTP interarrival jitter
# TYPE st2110_rtp_jitter_microseconds gauge
st2110_rtp_jitter_microseconds{multicast="239.1.1.10:20000",stream_id="cam1_vid",stream_name="Camera 1 - Video",type="video"} 287.3

# HELP st2110_rtp_bitrate_bps Bit/saniye cinsinden mevcut RTP stream bitrate
# TYPE st2110_rtp_bitrate_bps gauge
st2110_rtp_bitrate_bps{multicast="239.1.1.10:20000",stream_id="cam1_vid",stream_name="Camera 1 - Video",type="video"} 2197543936

# HELP st2110_rtp_packet_loss_rate Mevcut paket kayıp oranı (yüzde)
# TYPE st2110_rtp_packet_loss_rate gauge
st2110_rtp_packet_loss_rate{multicast="239.1.1.10:20000",stream_id="cam1_vid",stream_name="Camera 1 - Video",type="video"} 0.000221

4.2 PTP Exporter

PTP metrikleri için benzer yaklaşım:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// ptp/exporter.go
package main

import (
    "bufio"
    "fmt"
    "log"
    "net/http"
    "os/exec"
    "regexp"
    "strconv"
    "time"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

type PTPExporter struct {
    offsetFromMaster *prometheus.GaugeVec
    meanPathDelay    *prometheus.GaugeVec
    clockState       *prometheus.GaugeVec
    stepsRemoved     *prometheus.GaugeVec
}

func NewPTPExporter() *PTPExporter {
    exporter := &PTPExporter{
        offsetFromMaster: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_ptp_offset_nanoseconds",
                Help: "Nanosaniye cinsinden PTP master clock'tan offset",
            },
            []string{"device", "interface", "master"},
        ),
        meanPathDelay: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_ptp_mean_path_delay_nanoseconds",
                Help: "Nanosaniye cinsinden PTP master'a ortalama yol gecikmesi",
            },
            []string{"device", "interface", "master"},
        ),
        clockState: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_ptp_clock_state",
                Help: "PTP clock durumu (0=FREERUN, 1=LOCKED, 2=HOLDOVER)",
            },
            []string{"device", "interface"},
        ),
        stepsRemoved: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_ptp_steps_removed",
                Help: "Grandmaster clock'tan kaldırılan adımlar",
            },
            []string{"device", "interface"},
        ),
    }
    
    prometheus.MustRegister(exporter.offsetFromMaster)
    prometheus.MustRegister(exporter.meanPathDelay)
    prometheus.MustRegister(exporter.clockState)
    prometheus.MustRegister(exporter.stepsRemoved)
    
    return exporter
}

// ptpd veya ptp4l çıktısını parse et
func (e *PTPExporter) CollectPTPMetrics(device string, iface string) {
    // ptp4l yönetim sorgusunu çalıştır
    cmd := exec.Command("pmc", "-u", "-b", "0", "GET CURRENT_DATA_SET")
    output, err := cmd.CombinedOutput()
    if err != nil {
        log.Printf("PTP sorgulanamadı: %v", err)
        return
    }
    
    // Çıktıyı parse et (örnek format):
    // CURRENT_DATA_SET
    //   offsetFromMaster     125
    //   meanPathDelay        523
    //   stepsRemoved         1
    
    offsetRegex := regexp.MustCompile(`offsetFromMaster\s+(-?\d+)`)
    delayRegex := regexp.MustCompile(`meanPathDelay\s+(\d+)`)
    stepsRegex := regexp.MustCompile(`stepsRemoved\s+(\d+)`)
    
    outputStr := string(output)
    
    if matches := offsetRegex.FindStringSubmatch(outputStr); len(matches) > 1 {
        offset, _ := strconv.ParseFloat(matches[1], 64)
        e.offsetFromMaster.WithLabelValues(device, iface, "grandmaster").Set(offset)
    }
    
    if matches := delayRegex.FindStringSubmatch(outputStr); len(matches) > 1 {
        delay, _ := strconv.ParseFloat(matches[1], 64)
        e.meanPathDelay.WithLabelValues(device, iface, "grandmaster").Set(delay)
    }
    
    if matches := stepsRegex.FindStringSubmatch(outputStr); len(matches) > 1 {
        steps, _ := strconv.ParseFloat(matches[1], 64)
        e.stepsRemoved.WithLabelValues(device, iface).Set(steps)
    }
    
    // TODO: Clock state'i parse et (LOCKED, HOLDOVER, FREERUN)
    e.clockState.WithLabelValues(device, iface).Set(1)  // 1 = LOCKED
}

func (e *PTPExporter) Start(device string, iface string, interval time.Duration) {
    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            e.CollectPTPMetrics(device, iface)
        }
    }()
}

func main() {
    exporter := NewPTPExporter()
    exporter.Start("camera-1", "eth0", 1*time.Second)
    
    http.Handle("/metrics", promhttp.Handler())
    log.Fatal(http.ListenAndServe(":9200", nil))
}

4.3 Network Switch’leri için gNMI Collector

gNMI (gRPC Network Management Interface), SNMP’nin modern yerine geçenidir. Yüksek bant genişliği gereksinimleri olan ST 2110 sistemleri için gNMI şunları sağlar:

  • Streaming Telemetry: Gerçek zamanlı metrik push (SNMP polling’e karşı)
  • gRPC tabanlı: SNMP’den daha hızlı, daha verimli
  • YANG Modelleri: Yapılandırılmış, vendor-bağımsız veri modelleri
  • Saniye-Altı Güncellemeler: Ağ sorunlarını tespit etmek için kritik

ST 2110 için Neden gNMI?

Özellik SNMP (Eski) gNMI (Modern)
Protokol UDP/161 gRPC/TLS
Model Pull (30s+ polling) Push (streaming, saniye-altı)
Veri Formatı MIB (karmaşık) YANG/JSON (yapılandırılmış)
Performans Yavaş, yüksek overhead Hızlı, verimli
Güvenlik SNMPv3 (sınırlı) TLS + authentication
Switch Desteği Hepsi (legacy) Sadece modern (Arista, Cisco, Juniper)

ST 2110 Kullanım Durumu: Her biri 2.2Gbps olan 50+ multicast stream ile gerçek zamanlı switch metriklerine ihtiyacınız var. gNMI, port kullanımı, buffer drop’ları ve QoS istatistiklerini her 100ms’de stream edebilir, tıkanıklığın anında tespitine olanak tanır.

ST 2110 için Desteklenen Switch’ler

Vendor Model gNMI Desteği ST 2110 Uyumluluğu
Arista 7050X3, 7280R3 ✅ EOS 4.23+ ✅ Mükemmel (PTP, IGMP)
Cisco Nexus 9300/9500 ✅ NX-OS 9.3+ ✅ İyi (feature set gerektirir)
Juniper QFX5120, QFX5200 ✅ Junos 18.1+ ✅ İyi
Mellanox SN3700, SN4600 ✅ Onyx 3.9+ ✅ Mükemmel

ST 2110 için gNMI Path Örnekleri

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Abone olunacak kritik metrikler
subscriptions:
  # Interface istatistikleri
  - path: /interfaces/interface[name=*]/state/counters
    mode: SAMPLE
    interval: 1s
  
  # QoS buffer kullanımı (kritik!)
  - path: /qos/interfaces/interface[name=*]/output/queues/queue[name=*]/state
    mode: SAMPLE
    interval: 1s
  
  # IGMP multicast grupları
  - path: /network-instances/network-instance/protocols/protocol/igmp/interfaces
    mode: ON_CHANGE
  
  # PTP interface durumu (switch sağlıyorsa)
  - path: /system/ptp/interfaces/interface[name=*]/state
    mode: SAMPLE
    interval: 1s

Go ile gNMI Collector Implementasyonu

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
// gnmi/collector.go
package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "log"
    "net/http"
    "time"
    
    "github.com/openconfig/gnmi/proto/gnmi"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

type GNMICollector struct {
    target   string
    username string
    password string
    
    // Prometheus metrikleri
    interfaceRxBytes    *prometheus.GaugeVec
    interfaceTxBytes    *prometheus.GaugeVec
    interfaceRxErrors   *prometheus.GaugeVec
    interfaceTxErrors   *prometheus.GaugeVec
    interfaceRxDrops    *prometheus.GaugeVec
    interfaceTxDrops    *prometheus.GaugeVec
    qosBufferUtil       *prometheus.GaugeVec
    qosDroppedPackets   *prometheus.GaugeVec
    multicastGroups     *prometheus.GaugeVec
}

func NewGNMICollector(target, username, password string) *GNMICollector {
    collector := &GNMICollector{
        target:   target,
        username: username,
        password: password,
        
        interfaceRxBytes: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_interface_rx_bytes",
                Help: "Switch interface'inde alınan byte'lar",
            },
            []string{"switch", "interface"},
        ),
        
        interfaceTxBytes: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_interface_tx_bytes",
                Help: "Switch interface'inden iletilen byte'lar",
            },
            []string{"switch", "interface"},
        ),
        
        interfaceRxErrors: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_interface_rx_errors",
                Help: "Switch interface'inde alım hataları",
            },
            []string{"switch", "interface"},
        ),
        
        interfaceTxErrors: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_interface_tx_errors",
                Help: "Switch interface'inde iletim hataları",
            },
            []string{"switch", "interface"},
        ),
        
        interfaceRxDrops: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_interface_rx_drops",
                Help: "Switch interface'inde düşen alınan paketler",
            },
            []string{"switch", "interface"},
        ),
        
        interfaceTxDrops: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_interface_tx_drops",
                Help: "Switch interface'inde düşen iletilen paketler",
            },
            []string{"switch", "interface"},
        ),
        
        qosBufferUtil: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_qos_buffer_utilization",
                Help: "QoS buffer kullanım yüzdesi",
            },
            []string{"switch", "interface", "queue"},
        ),
        
        qosDroppedPackets: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_qos_dropped_packets",
                Help: "QoS nedeniyle düşen paketler",
            },
            []string{"switch", "interface", "queue"},
        ),
        
        multicastGroups: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_switch_multicast_groups",
                Help: "IGMP multicast grup sayısı",
            },
            []string{"switch", "interface"},
        ),
    }
    
    // Metrikleri kaydet
    prometheus.MustRegister(collector.interfaceRxBytes)
    prometheus.MustRegister(collector.interfaceTxBytes)
    prometheus.MustRegister(collector.interfaceRxErrors)
    prometheus.MustRegister(collector.interfaceTxErrors)
    prometheus.MustRegister(collector.interfaceRxDrops)
    prometheus.MustRegister(collector.interfaceTxDrops)
    prometheus.MustRegister(collector.qosBufferUtil)
    prometheus.MustRegister(collector.qosDroppedPackets)
    prometheus.MustRegister(collector.multicastGroups)
    
    return collector
}

func (c *GNMICollector) Connect() (gnmi.GNMIClient, error) {
    // TLS yapılandırması (lab için doğrulama atla, production'da düzgün sertifika kullan!)
    tlsConfig := &tls.Config{
        InsecureSkipVerify: true,  // ⚠️ Production'da düzgün sertifika kullan
    }
    
    // gRPC bağlantı seçenekleri
    opts := []grpc.DialOption{
        grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
        grpc.WithPerRPCCredentials(&loginCreds{
            Username: c.username,
            Password: c.password,
        }),
        grpc.WithBlock(),
        grpc.WithTimeout(10 * time.Second),
    }
    
    // gNMI hedefine bağlan
    conn, err := grpc.Dial(c.target, opts...)
    if err != nil {
        return nil, fmt.Errorf("%s'e bağlanılamadı: %w", c.target, err)
    }
    
    client := gnmi.NewGNMIClient(conn)
    log.Printf("gNMI hedefine bağlanıldı: %s", c.target)
    
    return client, nil
}

func (c *GNMICollector) Subscribe(ctx context.Context) error {
    client, err := c.Connect()
    if err != nil {
        return err
    }
    
    // Subscription request oluştur
    subscribeReq := &gnmi.SubscribeRequest{
        Request: &gnmi.SubscribeRequest_Subscribe{
            Subscribe: &gnmi.SubscriptionList{
                Mode: gnmi.SubscriptionList_STREAM,
                Subscription: []*gnmi.Subscription{
                    // Interface counter'ları
                    {
                        Path: &gnmi.Path{
                            Elem: []*gnmi.PathElem{
                                {Name: "interfaces"},
                                {Name: "interface", Key: map[string]string{"name": "*"}},
                                {Name: "state"},
                                {Name: "counters"},
                            },
                        },
                        Mode:           gnmi.SubscriptionMode_SAMPLE,
                        SampleInterval: 1000000000, // nanosaniye cinsinden 1 saniye
                    },
                    // QoS queue istatistikleri
                    {
                        Path: &gnmi.Path{
                            Elem: []*gnmi.PathElem{
                                {Name: "qos"},
                                {Name: "interfaces"},
                                {Name: "interface", Key: map[string]string{"name": "*"}},
                                {Name: "output"},
                                {Name: "queues"},
                                {Name: "queue", Key: map[string]string{"name": "*"}},
                                {Name: "state"},
                            },
                        },
                        Mode:           gnmi.SubscriptionMode_SAMPLE,
                        SampleInterval: 1000000000, // 1 saniye
                    },
                },
                Encoding: gnmi.Encoding_JSON_IETF,
            },
        },
    }
    
    // Subscription stream'i başlat
    stream, err := client.Subscribe(ctx)
    if err != nil {
        return fmt.Errorf("subscribe edilemedi: %w", err)
    }
    
    // Subscription request gönder
    if err := stream.Send(subscribeReq); err != nil {
        return fmt.Errorf("subscription gönderilemedi: %w", err)
    }
    
    log.Println("gNMI subscription stream'i başlatıldı")
    
    // Güncellemeleri al
    for {
        response, err := stream.Recv()
        if err != nil {
            return fmt.Errorf("stream hatası: %w", err)
        }
        
        c.handleUpdate(response)
    }
}

func (c *GNMICollector) handleUpdate(response *gnmi.SubscribeResponse) {
    switch resp := response.Response.(type) {
    case *gnmi.SubscribeResponse_Update:
        notification := resp.Update
        
        // Prefix'ten switch adını çıkar
        switchName := c.target
        
        for _, update := range notification.Update {
            path := update.Path
            value := update.Val
            
            // Interface counter'ları parse et
            if len(path.Elem) >= 4 && path.Elem[0].Name == "interfaces" {
                ifaceName := path.Elem[1].Key["name"]
                
                if path.Elem[2].Name == "state" && path.Elem[3].Name == "counters" {
                    // JSON'dan counter değerlerini parse et
                    if jsonVal := value.GetJsonIetfVal(); jsonVal != nil {
                        counters := parseCounters(jsonVal)
                        
                        c.interfaceRxBytes.WithLabelValues(switchName, ifaceName).Set(float64(counters.InOctets))
                        c.interfaceTxBytes.WithLabelValues(switchName, ifaceName).Set(float64(counters.OutOctets))
                        c.interfaceRxErrors.WithLabelValues(switchName, ifaceName).Set(float64(counters.InErrors))
                        c.interfaceTxErrors.WithLabelValues(switchName, ifaceName).Set(float64(counters.OutErrors))
                        c.interfaceRxDrops.WithLabelValues(switchName, ifaceName).Set(float64(counters.InDiscards))
                        c.interfaceTxDrops.WithLabelValues(switchName, ifaceName).Set(float64(counters.OutDiscards))
                    }
                }
            }
            
            // QoS queue istatistiklerini parse et
            if len(path.Elem) >= 7 && path.Elem[0].Name == "qos" {
                ifaceName := path.Elem[2].Key["name"]
                queueName := path.Elem[5].Key["name"]
                
                if jsonVal := value.GetJsonIetfVal(); jsonVal != nil {
                    qos := parseQoSStats(jsonVal)
                    
                    c.qosBufferUtil.WithLabelValues(switchName, ifaceName, queueName).Set(qos.BufferUtilization)
                    c.qosDroppedPackets.WithLabelValues(switchName, ifaceName, queueName).Set(float64(qos.DroppedPackets))
                }
            }
        }
    
    case *gnmi.SubscribeResponse_SyncResponse:
        log.Println("Sync response alındı (ilk senkronizasyon tamamlandı)")
    }
}

// Yardımcı yapılar
type InterfaceCounters struct {
    InOctets    uint64
    OutOctets   uint64
    InErrors    uint64
    OutErrors   uint64
    InDiscards  uint64
    OutDiscards uint64
}

type QoSStats struct {
    BufferUtilization float64
    DroppedPackets    uint64
}

func parseCounters(jsonData []byte) InterfaceCounters {
    // Counter'ları çıkarmak için JSON parse et
    // İmplementasyon switch'inizin YANG modeline bağlıdır
    var counters InterfaceCounters
    // ... JSON parsing mantığı ...
    return counters
}

func parseQoSStats(jsonData []byte) QoSStats {
    // QoS istatistiklerini parse et
    var qos QoSStats
    // ... JSON parsing mantığı ...
    return qos
}

// gRPC credentials yardımcısı
type loginCreds struct {
    Username string
    Password string
}

func (c *loginCreds) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    return map[string]string{
        "username": c.Username,
        "password": c.Password,
    }, nil
}

func (c *loginCreds) RequireTransportSecurity() bool {
    return true
}

func main() {
    // Yapılandırma
    switches := []struct {
        target   string
        username string
        password string
    }{
        {"core-switch-1.broadcast.local:6030", "admin", "password"},
        {"core-switch-2.broadcast.local:6030", "admin", "password"},
    }
    
    // Her switch için collector başlat
    for _, sw := range switches {
        collector := NewGNMICollector(sw.target, sw.username, sw.password)
        
        go func(c *GNMICollector) {
            ctx := context.Background()
            if err := c.Subscribe(ctx); err != nil {
                log.Printf("Subscription hatası: %v", err)
            }
        }(collector)
    }
    
    // Prometheus metriklerini açığa çıkar
    http.Handle("/metrics", promhttp.Handler())
    log.Println(":9273 üzerinde gNMI collector başlatılıyor")
    log.Fatal(http.ListenAndServe(":9273", nil))
}

Arista EOS için Yapılandırma

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Arista switch'te gNMI'yi etkinleştir
switch(config)# management api gnmi
switch(config-mgmt-api-gnmi)# transport grpc default
switch(config-mgmt-api-gnmi-transport-default)# ssl profile default
switch(config-mgmt-api-gnmi)# provider eos-native
switch(config-mgmt-api-gnmi)# exit

# gNMI erişimi için kullanıcı oluştur
switch(config)# username prometheus privilege 15 secret prometheus123

# gNMI'nin çalıştığını doğrula
switch# show management api gnmi

Cisco Nexus için Yapılandırma

1
2
3
4
5
6
7
8
# Cisco Nexus'ta gRPC'yi etkinleştir
switch(config)# feature grpc
switch(config)# grpc port 6030
switch(config)# grpc use-vrf management

# YANG model desteğini etkinleştir
switch(config)# feature nxapi
switch(config)# nxapi use-vrf management

4.4 Gelişmiş Vendor-Specific Entegrasyonlar

Arista EOS - Tam gNMI Yapılandırması

ST 2110 Optimizasyonları ile Production-Grade Kurulum:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
! Arista EOS 7280R3 - ST 2110 Optimized Configuration

! Enable gNMI with secure transport
management api gnmi
   transport grpc default
      vrf MGMT
      ssl profile BROADCAST_MONITORING
   provider eos-native
!
! Configure SSL profile for secure gNMI
management security
   ssl profile BROADCAST_MONITORING
      certificate monitoring-cert.crt key monitoring-key.key
      trust certificate ca-bundle.crt
!
! Create monitoring user with limited privileges
username prometheus privilege 15 role network-monitor secret sha512 $6$...
!
! Enable streaming telemetry for ST 2110 interfaces
management api gnmi
   transport grpc MONITORING
      port 6030
      vrf MGMT
      notification-timestamp send-time
!

ST 2110 için Arista-Specific gNMI Path’leri:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# arista-gnmi-paths.yaml
# Optimized for ST 2110 broadcast monitoring

subscriptions:
  # Interface statistics (1-second streaming)
  - path: /interfaces/interface[name=Ethernet1/1]/state/counters
    mode: SAMPLE
    interval: 1s
    
  # EOS-specific: Hardware queue drops (critical for ST 2110!)
  - path: /Arista/eos/arista-exp-eos-qos/qos/interfaces/interface[name=*]/queues/queue[queue-id=*]/state/dropped-pkts
    mode: SAMPLE
    interval: 1s
    
  # EOS-specific: PTP status (if using Arista as PTP Boundary Clock)
  - path: /Arista/eos/arista-exp-eos-ptp/ptp/instances/instance[instance-id=default]/state
    mode: ON_CHANGE
    
  # EOS-specific: IGMP snooping state
  - path: /Arista/eos/arista-exp-eos-igmpsnooping/igmp-snooping/vlans/vlan[vlan-id=100]/state
    mode: ON_CHANGE
    
  # Multicast routing table (ST 2110 streams)
  - path: /network-instances/network-instance[name=default]/protocols/protocol[identifier=IGMP]/igmp/interfaces
    mode: SAMPLE
    interval: 5s

Hardware Queue Monitoring ile Arista EOS gNMI Collector:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// arista/eos_collector.go
package arista

import (
    "context"
    "fmt"
    "github.com/openconfig/gnmi/proto/gnmi"
    "github.com/prometheus/client_golang/prometheus"
)

type AristaEOSCollector struct {
    *GNMICollector
    
    // Arista-specific metrics
    hwQueueDrops     *prometheus.CounterVec
    ptpLockStatus    *prometheus.GaugeVec
    igmpGroups       *prometheus.GaugeVec
    tcamUtilization  *prometheus.GaugeVec
}

func NewAristaEOSCollector(target, username, password string) *AristaEOSCollector {
    return &AristaEOSCollector{
        GNMICollector: NewGNMICollector(target, username, password),
        
        hwQueueDrops: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "arista_hw_queue_drops_total",
                Help: "Hardware queue drops (critical for ST 2110)",
            },
            []string{"switch", "interface", "queue"},
        ),
        
        ptpLockStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "arista_ptp_lock_status",
                Help: "PTP lock status (1=locked, 0=unlocked)",
            },
            []string{"switch", "domain"},
        ),
        
        igmpGroups: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "arista_igmp_snooping_groups",
                Help: "IGMP snooping multicast groups per VLAN",
            },
            []string{"switch", "vlan"},
        ),
        
        tcamUtilization: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "arista_tcam_utilization_percent",
                Help: "TCAM utilization (multicast routing table)",
            },
            []string{"switch", "table"},
        ),
    }
}

// Subscribe to Arista-specific paths
func (c *AristaEOSCollector) SubscribeArista(ctx context.Context) error {
    client, err := c.Connect()
    if err != nil {
        return err
    }
    
    // Arista EOS uses vendor-specific YANG models
    subscribeReq := &gnmi.SubscribeRequest{
        Request: &gnmi.SubscribeRequest_Subscribe{
            Subscribe: &gnmi.SubscriptionList{
                Mode: gnmi.SubscriptionList_STREAM,
                Subscription: []*gnmi.Subscription{
                    // Hardware queue drops (Arista-specific path)
                    {
                        Path: &gnmi.Path{
                            Origin: "arista",  // Arista vendor origin
                            Elem: []*gnmi.PathElem{
                                {Name: "eos"},
                                {Name: "arista-exp-eos-qos"},
                                {Name: "qos"},
                                {Name: "interfaces"},
                                {Name: "interface", Key: map[string]string{"name": "*"}},
                                {Name: "queues"},
                                {Name: "queue", Key: map[string]string{"queue-id": "*"}},
                                {Name: "state"},
                                {Name: "dropped-pkts"},
                            },
                        },
                        Mode:           gnmi.SubscriptionMode_SAMPLE,
                        SampleInterval: 1000000000, // 1 second
                    },
                },
                Encoding: gnmi.Encoding_JSON_IETF,
            },
        },
    }
    
    // Start subscription...
    stream, err := client.Subscribe(ctx)
    if err != nil {
        return fmt.Errorf("failed to subscribe: %w", err)
    }
    
    if err := stream.Send(subscribeReq); err != nil {
        return fmt.Errorf("failed to send subscription: %w", err)
    }
    
    // Process updates
    for {
        response, err := stream.Recv()
        if err != nil {
            return fmt.Errorf("stream error: %w", err)
        }
        
        c.handleAristaUpdate(response)
    }
}

func (c *AristaEOSCollector) handleAristaUpdate(response *gnmi.SubscribeResponse) {
    switch resp := response.Response.(type) {
    case *gnmi.SubscribeResponse_Update:
        notification := resp.Update
        
        for _, update := range notification.Update {
            path := update.Path
            value := update.Val
            
            // Parse Arista-specific hardware queue drops
            if path.Origin == "arista" && len(path.Elem) > 7 {
                if path.Elem[7].Name == "dropped-pkts" {
                    ifaceName := path.Elem[4].Key["name"]
                    queueID := path.Elem[6].Key["queue-id"]
                    
                    drops := value.GetUintVal()
                    c.hwQueueDrops.WithLabelValues(c.target, ifaceName, queueID).Add(float64(drops))
                    
                    // Alert if drops detected (should be ZERO for ST 2110!)
                    if drops > 0 {
                        fmt.Printf("⚠️  Hardware queue drops on %s interface %s queue %s: %d packets\n",
                            c.target, ifaceName, queueID, drops)
                    }
                }
            }
        }
    }
}

Cisco Nexus - Detaylı YANG Path Yapılandırması

Cisco NX-OS Specific gNMI Path’leri:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# cisco-nexus-gnmi-paths.yaml
# Cisco Nexus 9300/9500 for ST 2110

subscriptions:
  # Cisco DME (Data Management Engine) paths
  
  # Interface statistics (Cisco-specific)
  - path: /System/intf-items/phys-items/PhysIf-list[id=eth1/1]/dbgIfIn-items
    mode: SAMPLE
    interval: 1s
    
  # Cisco QoS policy statistics
  - path: /System/ipqos-items/queuing-items/policy-items/out-items/sys-items/pmap-items/Name-list[name=ST2110-OUT]/cmap-items/Name-list[name=VIDEO]/stats-items
    mode: SAMPLE
    interval: 1s
    
  # Cisco hardware TCAM usage (multicast routing)
  - path: /System/tcam-items/utilization-items
    mode: SAMPLE
    interval: 10s
    
  # IGMP snooping (Cisco-specific)
  - path: /System/igmpsn-items/inst-items/dom-items/Db-list[vlanId=100]
    mode: ON_CHANGE
    
  # Buffer statistics (critical for ST 2110)
  - path: /System/intf-items/phys-items/PhysIf-list[id=*]/buffer-items
    mode: SAMPLE
    interval: 1s

Cisco Nexus gNMI Collector:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// cisco/nexus_collector.go
package cisco

import (
    "encoding/json"
    "github.com/openconfig/gnmi/proto/gnmi"
    "github.com/prometheus/client_golang/prometheus"
)

type CiscoNexusCollector struct {
    target string
    
    // Cisco-specific metrics
    tcamUtilization  *prometheus.GaugeVec
    qosPolicyStats   *prometheus.CounterVec
    bufferDrops      *prometheus.CounterVec
    igmpVlans        *prometheus.GaugeVec
}

func NewCiscoNexusCollector(target, username, password string) *CiscoNexusCollector {
    return &CiscoNexusCollector{
        target: target,
        
        tcamUtilization: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "cisco_nexus_tcam_utilization_percent",
                Help: "TCAM utilization for multicast routing",
            },
            []string{"switch", "table_type"},
        ),
        
        qosPolicyStats: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "cisco_nexus_qos_policy_drops_total",
                Help: "QoS policy drops (by class-map)",
            },
            []string{"switch", "policy", "class"},
        ),
        
        bufferDrops: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "cisco_nexus_buffer_drops_total",
                Help: "Interface buffer drops",
            },
            []string{"switch", "interface"},
        ),
    }
}

// Cisco DME (Data Management Engine) JSON parsing
func (c *CiscoNexusCollector) parseCiscoDME(jsonData []byte) {
    var dme struct {
        Imdata []struct {
            DbgIfIn struct {
                Attributes struct {
                    InOctets  string `json:"inOctets"`
                    InErrors  string `json:"inErrors"`
                    InDrops   string `json:"inDrops"`
                } `json:"attributes"`
            } `json:"dbgIfIn"`
        } `json:"imdata"`
    }
    
    json.Unmarshal(jsonData, &dme)
    
    // Parse and expose metrics...
}

Grass Valley K-Frame - REST API Entegrasyonu

K-Frame Sistem İzleme:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
// grassvalley/kframe_exporter.go
package grassvalley

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
    "github.com/prometheus/client_golang/prometheus"
)

type KFrameExporter struct {
    baseURL  string  // http://kframe-ip
    apiKey   string
    
    // K-Frame specific metrics
    cardStatus        *prometheus.GaugeVec
    cardTemperature   *prometheus.GaugeVec
    videoInputStatus  *prometheus.GaugeVec
    audioChannelStatus *prometheus.GaugeVec
    crosspointStatus  *prometheus.GaugeVec
}

func NewKFrameExporter(baseURL, apiKey string) *KFrameExporter {
    return &KFrameExporter{
        baseURL: baseURL,
        apiKey:  apiKey,
        
        cardStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_kframe_card_status",
                Help: "K-Frame card status (1=OK, 0=fault)",
            },
            []string{"chassis", "slot", "card_type"},
        ),
        
        cardTemperature: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_kframe_card_temperature_celsius",
                Help: "K-Frame card temperature",
            },
            []string{"chassis", "slot", "card_type"},
        ),
        
        videoInputStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_kframe_video_input_status",
                Help: "Video input signal status (1=present, 0=no signal)",
            },
            []string{"chassis", "slot", "input"},
        ),
        
        audioChannelStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_kframe_audio_channel_status",
                Help: "Audio channel status (1=present, 0=silent)",
            },
            []string{"chassis", "slot", "channel"},
        ),
        
        crosspointStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_kframe_crosspoint_count",
                Help: "Number of active crosspoints (router connections)",
            },
            []string{"chassis", "router_level"},
        ),
    }
}

// K-Frame REST API endpoints
func (e *KFrameExporter) Collect() error {
    // Get chassis inventory
    chassis, err := e.getChassis()
    if err != nil {
        return err
    }
    
    for _, ch := range chassis {
        // Get card status for each slot
        cards, err := e.getCards(ch.ID)
        if err != nil {
            continue
        }
        
        for _, card := range cards {
            // Update card status
            e.cardStatus.WithLabelValues(ch.Name, card.Slot, card.Type).Set(boolToFloat(card.Healthy))
            e.cardTemperature.WithLabelValues(ch.Name, card.Slot, card.Type).Set(card.Temperature)
            
            // Get video input status (for ST 2110 receivers)
            if card.Type == "IPDENSITY" || card.Type == "IPG-3901" {
                inputs, err := e.getVideoInputs(ch.ID, card.Slot)
                if err != nil {
                    continue
                }
                
                for _, input := range inputs {
                    e.videoInputStatus.WithLabelValues(
                        ch.Name, card.Slot, input.Name,
                    ).Set(boolToFloat(input.SignalPresent))
                }
            }
        }
        
        // Get router crosspoint count
        crosspoints, err := e.getCrosspoints(ch.ID)
        if err != nil {
            continue
        }
        
        e.crosspointStatus.WithLabelValues(ch.Name, "video").Set(float64(crosspoints.VideoCount))
        e.crosspointStatus.WithLabelValues(ch.Name, "audio").Set(float64(crosspoints.AudioCount))
    }
    
    return nil
}

// K-Frame REST API client methods
func (e *KFrameExporter) makeRequest(endpoint string) ([]byte, error) {
    url := fmt.Sprintf("%s/api/v2/%s", e.baseURL, endpoint)
    
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("X-API-Key", e.apiKey)
    req.Header.Set("Accept", "application/json")
    
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body := make([]byte, resp.ContentLength)
    resp.Body.Read(body)
    
    return body, nil
}

func (e *KFrameExporter) getChassis() ([]Chassis, error) {
    data, err := e.makeRequest("chassis")
    if err != nil {
        return nil, err
    }
    
    var result struct {
        Chassis []Chassis `json:"chassis"`
    }
    json.Unmarshal(data, &result)
    
    return result.Chassis, nil
}

func (e *KFrameExporter) getCards(chassisID string) ([]Card, error) {
    data, err := e.makeRequest(fmt.Sprintf("chassis/%s/cards", chassisID))
    if err != nil {
        return nil, err
    }
    
    var result struct {
        Cards []Card `json:"cards"`
    }
    json.Unmarshal(data, &result)
    
    return result.Cards, nil
}

func (e *KFrameExporter) getVideoInputs(chassisID, slot string) ([]VideoInput, error) {
    endpoint := fmt.Sprintf("chassis/%s/cards/%s/inputs", chassisID, slot)
    data, err := e.makeRequest(endpoint)
    if err != nil {
        return nil, err
    }
    
    var result struct {
        Inputs []VideoInput `json:"inputs"`
    }
    json.Unmarshal(data, &result)
    
    return result.Inputs, nil
}

type Chassis struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

type Card struct {
    Slot        string  `json:"slot"`
    Type        string  `json:"type"`
    Healthy     bool    `json:"healthy"`
    Temperature float64 `json:"temperature"`
}

type VideoInput struct {
    Name          string `json:"name"`
    SignalPresent bool   `json:"signal_present"`
    Format        string `json:"format"`
}

func boolToFloat(b bool) float64 {
    if b {
        return 1
    }
    return 0
}

Evertz EQX/VIP - SNMP ve Proprietary API

Evertz İzleme Entegrasyonu:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// evertz/eqx_exporter.go
package evertz

import (
    "encoding/xml"
    "fmt"
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/gosnmp/gosnmp"
)

type EvertzEQXExporter struct {
    target     string
    snmp       *gosnmp.GoSNMP
    httpClient *http.Client
    
    // Evertz-specific metrics
    moduleStatus      *prometheus.GaugeVec
    ipFlowStatus      *prometheus.GaugeVec
    videoStreamStatus *prometheus.GaugeVec
    ptpStatus         *prometheus.GaugeVec
    redundancyStatus  *prometheus.GaugeVec
}

func NewEvertzEQXExporter(target, snmpCommunity string) *EvertzEQXExporter {
    snmp := &gosnmp.GoSNMP{
        Target:    target,
        Port:      161,
        Community: snmpCommunity,
        Version:   gosnmp.Version2c,
        Timeout:   5 * time.Second,
    }
    
    return &EvertzEQXExporter{
        target:     target,
        snmp:       snmp,
        httpClient: &http.Client{Timeout: 10 * time.Second},
        
        moduleStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "evertz_eqx_module_status",
                Help: "EQX module status (1=OK, 0=fault)",
            },
            []string{"chassis", "slot", "module_type"},
        ),
        
        ipFlowStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "evertz_eqx_ip_flow_status",
                Help: "IP flow status (1=active, 0=inactive)",
            },
            []string{"chassis", "flow_id", "direction"},
        ),
        
        videoStreamStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "evertz_eqx_video_stream_status",
                Help: "Video stream status (1=present, 0=no signal)",
            },
            []string{"chassis", "stream_id"},
        ),
        
        ptpStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "evertz_eqx_ptp_lock_status",
                Help: "PTP lock status (1=locked, 0=unlocked)",
            },
            []string{"chassis", "module"},
        ),
        
        redundancyStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "evertz_eqx_redundancy_status",
                Help: "Redundancy status (1=protected, 0=unprotected)",
            },
            []string{"chassis", "pair"},
        ),
    }
}

// Evertz EQX uses both SNMP and HTTP XML API
func (e *EvertzEQXExporter) Collect() error {
    // Connect SNMP
    if err := e.snmp.Connect(); err != nil {
        return err
    }
    defer e.snmp.Conn.Close()
    
    // Walk Evertz MIB tree
    if err := e.collectSNMP(); err != nil {
        return err
    }
    
    // Get detailed status via HTTP XML API
    if err := e.collectHTTPAPI(); err != nil {
        return err
    }
    
    return nil
}

// Evertz-specific SNMP OIDs
const (
    evertzModuleStatusOID = ".1.3.6.1.4.1.6827.20.1.1.1.1.2"  // evModule Status
    evertzIPFlowStatusOID = ".1.3.6.1.4.1.6827.20.2.1.1.1.5"  // evIPFlow Status
    evertzPTPLockOID      = ".1.3.6.1.4.1.6827.20.3.1.1.1.3"  // evPTP Lock Status
)

func (e *EvertzEQXExporter) collectSNMP() error {
    // Walk module status
    err := e.snmp.Walk(evertzModuleStatusOID, func(pdu gosnmp.SnmpPDU) error {
        // Parse OID to extract chassis/slot
        chassis, slot := parseEvertzOID(pdu.Name)
        status := pdu.Value.(int)
        
        e.moduleStatus.WithLabelValues(chassis, slot, "unknown").Set(float64(status))
        return nil
    })
    
    return err
}

func (e *EvertzEQXExporter) collectHTTPAPI() error {
    // Evertz XML API endpoint
    url := fmt.Sprintf("http://%s/status.xml", e.target)
    
    resp, err := e.httpClient.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    var status EvertzStatus
    if err := xml.NewDecoder(resp.Body).Decode(&status); err != nil {
        return err
    }
    
    // Update Prometheus metrics from XML
    for _, flow := range status.IPFlows {
        e.ipFlowStatus.WithLabelValues(
            status.Chassis,
            flow.ID,
            flow.Direction,
        ).Set(boolToFloat(flow.Active))
    }
    
    return nil
}

type EvertzStatus struct {
    Chassis string     `xml:"chassis,attr"`
    IPFlows []IPFlow   `xml:"ipflows>flow"`
}

type IPFlow struct {
    ID        string `xml:"id,attr"`
    Direction string `xml:"direction,attr"`
    Active    bool   `xml:"active"`
}

func parseEvertzOID(oid string) (chassis, slot string) {
    // Parse Evertz OID format
    // Example: .1.3.6.1.4.1.6827.20.1.1.1.1.2.1.5 -> chassis 1, slot 5
    return "1", "5"  // Simplified
}

Lawo VSM - Control System Entegrasyonu

VSM REST API İzleme:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// lawo/vsm_exporter.go
package lawo

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
    "github.com/prometheus/client_golang/prometheus"
)

type LawoVSMExporter struct {
    baseURL  string  // http://vsm-server:9000
    apiToken string
    
    // VSM-specific metrics
    connectionStatus  *prometheus.GaugeVec
    deviceStatus      *prometheus.GaugeVec
    pathwayStatus     *prometheus.GaugeVec
    alarmCount        *prometheus.GaugeVec
}

func NewLawoVSMExporter(baseURL, apiToken string) *LawoVSMExporter {
    return &LawoVSMExporter{
        baseURL:  baseURL,
        apiToken: apiToken,
        
        connectionStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "lawo_vsm_connection_status",
                Help: "VSM connection status (1=connected, 0=disconnected)",
            },
            []string{"device_name", "device_type"},
        ),
        
        deviceStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "lawo_vsm_device_status",
                Help: "Device status (1=OK, 0=fault)",
            },
            []string{"device_name", "device_type"},
        ),
        
        pathwayStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "lawo_vsm_pathway_status",
                Help: "Signal pathway status (1=active, 0=inactive)",
            },
            []string{"pathway_name", "source", "destination"},
        ),
        
        alarmCount: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "lawo_vsm_active_alarms",
                Help: "Number of active alarms",
            },
            []string{"severity"},
        ),
    }
}

func (e *LawoVSMExporter) Collect() error {
    // Get device tree from VSM
    devices, err := e.getDevices()
    if err != nil {
        return err
    }
    
    for _, device := range devices {
        e.deviceStatus.WithLabelValues(device.Name, device.Type).Set(
            boolToFloat(device.Status == "OK"),
        )
        e.connectionStatus.WithLabelValues(device.Name, device.Type).Set(
            boolToFloat(device.Connected),
        )
    }
    
    // Get active pathways
    pathways, err := e.getPathways()
    if err != nil {
        return err
    }
    
    for _, pathway := range pathways {
        e.pathwayStatus.WithLabelValues(
            pathway.Name,
            pathway.Source,
            pathway.Destination,
        ).Set(boolToFloat(pathway.Active))
    }
    
    // Get alarm summary
    alarms, err := e.getAlarms()
    if err != nil {
        return err
    }
    
    alarmCounts := map[string]int{"critical": 0, "warning": 0, "info": 0}
    for _, alarm := range alarms {
        alarmCounts[alarm.Severity]++
    }
    
    for severity, count := range alarmCounts {
        e.alarmCount.WithLabelValues(severity).Set(float64(count))
    }
    
    return nil
}

// VSM REST API client
func (e *LawoVSMExporter) makeRequest(endpoint string) ([]byte, error) {
    url := fmt.Sprintf("%s/api/v1/%s", e.baseURL, endpoint)
    
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("Authorization", "Bearer "+e.apiToken)
    req.Header.Set("Accept", "application/json")
    
    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body := make([]byte, resp.ContentLength)
    resp.Body.Read(body)
    
    return body, nil
}

func (e *LawoVSMExporter) getDevices() ([]VSMDevice, error) {
    data, err := e.makeRequest("devices")
    if err != nil {
        return nil, err
    }
    
    var result struct {
        Devices []VSMDevice `json:"devices"`
    }
    json.Unmarshal(data, &result)
    
    return result.Devices, nil
}

func (e *LawoVSMExporter) getPathways() ([]VSMPathway, error) {
    data, err := e.makeRequest("pathways")
    if err != nil {
        return nil, err
    }
    
    var result struct {
        Pathways []VSMPathway `json:"pathways"`
    }
    json.Unmarshal(data, &result)
    
    return result.Pathways, nil
}

func (e *LawoVSMExporter) getAlarms() ([]VSMAlarm, error) {
    data, err := e.makeRequest("alarms?state=active")
    if err != nil {
        return nil, err
    }
    
    var result struct {
        Alarms []VSMAlarm `json:"alarms"`
    }
    json.Unmarshal(data, &result)
    
    return result.Alarms, nil
}

type VSMDevice struct {
    Name      string `json:"name"`
    Type      string `json:"type"`
    Status    string `json:"status"`
    Connected bool   `json:"connected"`
}

type VSMPathway struct {
    Name        string `json:"name"`
    Source      string `json:"source"`
    Destination string `json:"destination"`
    Active      bool   `json:"active"`
}

type VSMAlarm struct {
    Severity string `json:"severity"`
    Message  string `json:"message"`
    Device   string `json:"device"`
}

Build ve Çalıştırma

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Bağımlılıkları kur
go get github.com/openconfig/gnmi
go get google.golang.org/grpc
go get github.com/prometheus/client_golang

# Build et
go build -o gnmi-collector gnmi/collector.go

# Çalıştır
./gnmi-collector

# Metrikleri test et
curl http://localhost:9273/metrics

Örnek Metrik Çıktısı:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# HELP st2110_switch_interface_rx_bytes Switch interface'inde alınan byte'lar
# TYPE st2110_switch_interface_rx_bytes gauge
st2110_switch_interface_rx_bytes{interface="Ethernet1",switch="core-switch-1"} 2.847392847e+12

# HELP st2110_switch_qos_buffer_utilization QoS buffer kullanım yüzdesi
# TYPE st2110_switch_qos_buffer_utilization gauge
st2110_switch_qos_buffer_utilization{interface="Ethernet1",queue="video-priority",switch="core-switch-1"} 45.2

# HELP st2110_switch_qos_dropped_packets QoS nedeniyle düşen paketler
# TYPE st2110_switch_qos_dropped_packets gauge
st2110_switch_qos_dropped_packets{interface="Ethernet1",queue="video-priority",switch="core-switch-1"} 0

Bu ST 2110 için Neden Önemli

Gerçek Dünya Senaryosu: 100Gbps core switch’ten geçen 50 kamera feed’iniz var (50 × 2.2Gbps = 110Gbps toplam).

SNMP ile (her 30s polling):

  • ❌ T+0s’de ağ tıkanıklığı oluşur
  • ❌ T+30s’de SNMP poll tespit eder
  • ❌ 30 saniye paket kaybı = felaket

gNMI ile (her 1s streaming):

  • ✅ T+0s’de ağ tıkanıklığı oluşur
  • ✅ T+1s’de gNMI güncellemesi tespit eder
  • ✅ T+2s’de uyarı tetiklenir
  • ✅ T+3s’de oto-iyileştirme (load balancing)
  • Minimal etki

4.4 Exporter’ları Deploy Etme

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Her ST 2110 receiver/cihazda:
# 1. Exporter'ları kur
sudo cp st2110-exporter /usr/local/bin/
sudo cp ptp-exporter /usr/local/bin/
sudo cp gnmi-collector /usr/local/bin/

# 2. RTP exporter için systemd servisi oluştur
sudo tee /etc/systemd/system/st2110-exporter.service <<EOF
[Unit]
Description=ST 2110 RTP Stream Exporter
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/st2110-exporter --config /etc/st2110/streams.yaml --listen :9100
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# 3. gNMI collector için systemd servisi oluştur
sudo tee /etc/systemd/system/gnmi-collector.service <<EOF
[Unit]
Description=gNMI Network Switch Collector
After=network.target

[Service]
Type=simple
User=gnmi
ExecStart=/usr/local/bin/gnmi-collector
Restart=always
Environment="GNMI_TARGETS=core-switch-1.local:6030,core-switch-2.local:6030"
Environment="GNMI_USERNAME=prometheus"
Environment="GNMI_PASSWORD=secure-password"

[Install]
WantedBy=multi-user.target
EOF

# 4. Tüm servisleri etkinleştir ve başlat
sudo systemctl enable st2110-exporter ptp-exporter gnmi-collector
sudo systemctl start st2110-exporter ptp-exporter gnmi-collector

# 5. Doğrula
curl http://localhost:9100/metrics  # RTP metrikleri
curl http://localhost:9200/metrics  # PTP metrikleri
curl http://localhost:9273/metrics  # Switch/network metrikleri

5. Grafana: Görselleştirme ve Dashboard’lar

5.1 Grafana Kurulumu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# docker-compose.yml (mevcut'a ekle)
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false

volumes:
  grafana_data:

5.2 Prometheus’u Veri Kaynağı Olarak Ekleme

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# grafana/provisioning/datasources/prometheus.yaml
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false

5.3 ST 2110 Dashboard Oluşturma (Temel)

Grafana’da yeni bir dashboard oluşturun ve aşağıdaki panelleri ekleyin:

Panel 1: Paket Kaybı (Tüm Stream’ler)

1
2
# PromQL Query
rate(st2110_rtp_packets_lost_total[1m])

5.3 Tam Production Dashboard (İçe Aktarılabilir)

İşte doğrudan içe aktarabileceğiniz tam, production-ready Grafana dashboard:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
{
  "dashboard": {
    "id": null,
    "uid": "st2110-monitoring",
    "title": "ST 2110 Production Monitoring",
    "tags": ["st2110", "broadcast", "production"],
    "timezone": "browser",
    "schemaVersion": 38,
    "version": 1,
    "refresh": "1s",
    "time": {
      "from": "now-15m",
      "to": "now"
    },
    "timepicker": {
      "refresh_intervals": ["1s", "5s", "10s", "30s", "1m"],
      "time_options": ["5m", "15m", "1h", "6h", "12h", "24h"]
    },
    "templating": {
      "list": [
        {
          "name": "stream",
          "type": "query",
          "datasource": "Prometheus",
          "query": "label_values(st2110_rtp_packets_received_total, stream_name)",
          "multi": true,
          "includeAll": true,
          "allValue": ".*",
          "refresh": 1
        },
        {
          "name": "switch",
          "type": "query",
          "datasource": "Prometheus",
          "query": "label_values(st2110_switch_interface_rx_bytes, switch)",
          "multi": true,
          "includeAll": true,
          "refresh": 1
        }
      ]
    },
    "panels": [
      {
        "id": 1,
        "gridPos": {"h": 8, "w": 12, "x": 0, "y": 0},
        "type": "stat",
        "title": "Critical Alerts",
        "targets": [
          {
            "expr": "count(ALERTS{alertstate=\"firing\",severity=\"critical\"})",
            "legendFormat": "Critical Alerts"
          }
        ],
        "options": {
          "colorMode": "background",
          "graphMode": "none",
          "orientation": "auto",
          "textMode": "value_and_name"
        },
        "fieldConfig": {
          "defaults": {
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": 0, "color": "green"},
                {"value": 1, "color": "red"}
              ]
            }
          }
        }
      },
      {
        "id": 2,
        "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0},
        "type": "stat",
        "title": "Active Streams",
        "targets": [
          {
            "expr": "count(rate(st2110_rtp_packets_received_total[30s]) > 0)",
            "legendFormat": "Active Streams"
          }
        ],
        "options": {
          "colorMode": "value",
          "graphMode": "area",
          "textMode": "value_and_name"
        }
      },
      {
        "id": 3,
        "gridPos": {"h": 10, "w": 24, "x": 0, "y": 8},
        "type": "timeseries",
        "title": "RTP Packet Loss Rate (%)",
        "targets": [
          {
            "expr": "st2110_rtp_packet_loss_rate{stream_name=~\"$stream\"}",
            "legendFormat": "{{stream_name}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "percent",
            "decimals": 4,
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": 0, "color": "green"},
                {"value": 0.001, "color": "yellow"},
                {"value": 0.01, "color": "red"}
              ]
            },
            "custom": {
              "drawStyle": "line",
              "lineInterpolation": "linear",
              "fillOpacity": 10,
              "showPoints": "never"
            }
          }
        },
        "options": {
          "tooltip": {"mode": "multi"},
          "legend": {"displayMode": "table", "placement": "right"}
        }
      },
      {
        "id": 4,
        "gridPos": {"h": 10, "w": 12, "x": 0, "y": 18},
        "type": "timeseries",
        "title": "RTP Jitter (μs)",
        "targets": [
          {
            "expr": "st2110_rtp_jitter_microseconds{stream_name=~\"$stream\"}",
            "legendFormat": "{{stream_name}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "µs",
            "decimals": 1,
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": 0, "color": "green"},
                {"value": 500, "color": "yellow"},
                {"value": 1000, "color": "red"}
              ]
            }
          }
        }
      },
      {
        "id": 5,
        "gridPos": {"h": 10, "w": 12, "x": 12, "y": 18},
        "type": "timeseries",
        "title": "PTP Offset from Master (μs)",
        "targets": [
          {
            "expr": "st2110_ptp_offset_nanoseconds / 1000",
            "legendFormat": "{{device}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "µs",
            "decimals": 2,
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": -10, "color": "red"},
                {"value": -1, "color": "yellow"},
                {"value": 1, "color": "green"},
                {"value": 10, "color": "yellow"}
              ]
            }
          }
        }
      },
      {
        "id": 6,
        "gridPos": {"h": 10, "w": 12, "x": 0, "y": 28},
        "type": "timeseries",
        "title": "Stream Bitrate (Gbps)",
        "targets": [
          {
            "expr": "st2110_rtp_bitrate_bps{stream_name=~\"$stream\"} / 1e9",
            "legendFormat": "{{stream_name}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "Gbps",
            "decimals": 2
          }
        }
      },
      {
        "id": 7,
        "gridPos": {"h": 10, "w": 12, "x": 12, "y": 28},
        "type": "timeseries",
        "title": "Switch Port Utilization (%)",
        "targets": [
          {
            "expr": "rate(st2110_switch_interface_tx_bytes{switch=~\"$switch\"}[1m]) * 8 / 10e9 * 100",
            "legendFormat": "{{switch}} - {{interface}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "percent",
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": 0, "color": "green"},
                {"value": 80, "color": "yellow"},
                {"value": 90, "color": "red"}
              ]
            }
          }
        }
      },
      {
        "id": 8,
        "gridPos": {"h": 10, "w": 12, "x": 0, "y": 38},
        "type": "timeseries",
        "title": "VRX Buffer Level (ms)",
        "targets": [
          {
            "expr": "st2110_vrx_buffer_level_microseconds{stream_name=~\"$stream\"} / 1000",
            "legendFormat": "{{stream_name}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "ms",
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": 0, "color": "red"},
                {"value": 20, "color": "yellow"},
                {"value": 30, "color": "green"}
              ]
            }
          }
        }
      },
      {
        "id": 9,
        "gridPos": {"h": 10, "w": 12, "x": 12, "y": 38},
        "type": "timeseries",
        "title": "TR-03 Compliance Score",
        "targets": [
          {
            "expr": "st2110_tr03_c_v_mean{stream_name=~\"$stream\"}",
            "legendFormat": "{{stream_name}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "percentunit",
            "min": 0,
            "max": 1,
            "thresholds": {
              "mode": "absolute",
              "steps": [
                {"value": 0, "color": "red"},
                {"value": 0.5, "color": "yellow"},
                {"value": 0.8, "color": "green"}
              ]
            }
          }
        }
      },
      {
        "id": 10,
        "gridPos": {"h": 10, "w": 12, "x": 0, "y": 48},
        "type": "timeseries",
        "title": "IGMP Active Groups",
        "targets": [
          {
            "expr": "st2110_igmp_active_groups",
            "legendFormat": "{{vlan}} - {{interface}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "short"
          }
        }
      },
      {
        "id": 11,
        "gridPos": {"h": 10, "w": 12, "x": 12, "y": 48},
        "type": "timeseries",
        "title": "QoS Dropped Packets",
        "targets": [
          {
            "expr": "rate(st2110_switch_qos_dropped_packets{switch=~\"$switch\"}[1m])",
            "legendFormat": "{{switch}} - {{interface}} - {{queue}}"
          }
        ],
        "fieldConfig": {
          "defaults": {
            "unit": "pps"
          }
        }
      },
      {
        "id": 12,
        "gridPos": {"h": 8, "w": 24, "x": 0, "y": 58},
        "type": "table",
        "title": "Stream Health Summary",
        "targets": [
          {
            "expr": "st2110_rtp_packets_received_total{stream_name=~\"$stream\"}",
            "format": "table",
            "instant": true
          }
        ],
        "transformations": [
          {
            "id": "organize",
            "options": {
              "excludeByName": {
                "Time": true,
                "__name__": true
              },
              "indexByName": {
                "stream_name": 0,
                "multicast": 1,
                "Value": 2
              },
              "renameByName": {
                "stream_name": "Stream",
                "multicast": "Multicast",
                "Value": "Packets RX"
              }
            }
          }
        ],
        "options": {
          "showHeader": true,
          "sortBy": [{"displayName": "Packets RX", "desc": true}]
        }
      }
    ],
    "annotations": {
      "list": [
        {
          "datasource": "Prometheus",
          "enable": true,
          "expr": "ALERTS{alertstate=\"firing\"}",
          "name": "Alerts",
          "iconColor": "red"
        }
      ]
    }
  }
}

Dashboard’u İçe Aktarmak İçin:

  1. Grafana’yı açın → Dashboards → Import
  2. Yukarıdaki JSON’u kopyalayın
  3. Yapıştırın ve “Load"a tıklayın
  4. Prometheus veri kaynağını seçin
  5. “Import"a tıklayın

İndirme Linki: Yukarıdaki JSON’u st2110-dashboard.json olarak kaydedin (çevrimdışı kullanım için).

5.4 Özel Panel’ler Oluşturma

Single Stat Panel: Mevcut Paket Kaybı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "type": "singlestat",
  "title": "Current Packet Loss (Worst Stream)",
  "targets": [
    {
      "expr": "max(st2110_rtp_packet_loss_rate)"
    }
  ],
  "format": "percent",
  "decimals": 4,
  "thresholds": "0.001,0.01",
  "colors": ["green", "yellow", "red"],
  "sparkline": {
    "show": true
  }
}

Table Panel: Tüm Stream’lere Genel Bakış

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "type": "table",
  "title": "ST 2110 Streams Summary",
  "targets": [
    {
      "expr": "st2110_rtp_packets_received_total",
      "format": "table",
      "instant": true
    }
  ],
  "transformations": [
    {
      "id": "merge",
      "options": {}
    }
  ],
  "columns": [
    {"text": "Stream", "value": "stream_name"},
    {"text": "Packets RX", "value": "Value"},
    {"text": "Loss Rate", "value": "st2110_rtp_packet_loss_rate"}
  ]
}

6. Alert Kuralları ve Bildirimler

Complete Alert Flow Architecture

Alert Routing Decision Tree:

Alert Severity Classification:

🔴 CRITICAL (Immediate action required)

  • Packet loss > 0.01%
  • PTP offset > 50µs
  • Stream completely down
  • NMOS registry unavailable
  • SMPTE 2022-7: Both paths down → PagerDuty (immediate), Slack, Email, Phone (if no ACK in 5 min)

🟠 WARNING (Action required, not urgent)

  • Packet loss > 0.001%
  • PTP offset > 10µs
  • Jitter > 500µs
  • Buffer utilization > 80%
  • Single path down (2022-7 protection active) → Slack, Email (no page)

🟡 INFO (Awareness, no immediate action)

  • Capacity planning alerts
  • Performance degradation trends
  • Configuration changes
  • Scheduled maintenance reminders → Slack only (low priority channel)

6.1 Prometheus Alert Kuralları

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# alerts/st2110.yml
groups:
  - name: st2110_alerts
    interval: 1s  # Her saniye değerlendir
    rules:
      # Kritik: Paket kaybı > %0.01
      - alert: ST2110HighPacketLoss
        expr: st2110_rtp_packet_loss_rate > 0.01
        for: 5s
        labels:
          severity: critical
          team: broadcast
        annotations:
          summary: "{{$labels.stream_name}} üzerinde yüksek paket kaybı"
          description: "Stream {{$labels.stream_name}}'de {{$value}}% paket kaybı var (eşik: %0.01)"
      
      # Uyarı: Paket kaybı > %0.001
      - alert: ST2110ModeratePacketLoss
        expr: st2110_rtp_packet_loss_rate > 0.001 and st2110_rtp_packet_loss_rate <= 0.01
        for: 10s
        labels:
          severity: warning
          team: broadcast
        annotations:
          summary: "{{$labels.stream_name}} üzerinde orta seviye paket kaybı"
          description: "Stream {{$labels.stream_name}}'de {{$value}}% paket kaybı var"
      
      # Kritik: Yüksek jitter
      - alert: ST2110HighJitter
        expr: st2110_rtp_jitter_microseconds > 1000
        for: 10s
        labels:
          severity: critical
        annotations:
          summary: "{{$labels.stream_name}} üzerinde yüksek jitter"
          description: "Stream {{$labels.stream_name}} jitter'ı {{$value}}μs (eşik: 1000μs)"
      
      # Kritik: PTP offset
      - alert: ST2110PTPOffsetHigh
        expr: abs(st2110_ptp_offset_nanoseconds) > 10000
        for: 5s
        labels:
          severity: critical
        annotations:
          summary: "{{$labels.device}} üzerinde yüksek PTP offset"
          description: "Cihaz {{$labels.device}} PTP offset'i {{$value}}ns (eşik: 10μs)"
      
      # Kritik: Stream çöktü
      - alert: ST2110StreamDown
        expr: rate(st2110_rtp_packets_received_total[30s]) == 0
        for: 10s
        labels:
          severity: critical
        annotations:
          summary: "ST 2110 stream {{$labels.stream_name}} çöktü"
          description: "30 saniyedir paket alınmadı"

6.2 Alertmanager Yapılandırması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# alertmanager.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'cluster']
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 12h
  receiver: 'broadcast-team'
  
  routes:
    # Kritik uyarılar PagerDuty'ye
    - match:
        severity: critical
      receiver: 'pagerduty'
      continue: true
    
    # Tüm uyarılar Slack'e
    - match_re:
        severity: ^(warning|critical)$
      receiver: 'slack'

receivers:
  - name: 'broadcast-team'
    email_configs:
      - to: 'broadcast-ops@company.com'
        from: 'prometheus@company.com'
        smarthost: 'smtp.company.com:587'
  
  - name: 'pagerduty'
    pagerduty_configs:
      - service_key: 'YOUR_PAGERDUTY_KEY'
        description: '{{ .CommonAnnotations.summary }}'
  
  - name: 'slack'
    slack_configs:
      - api_url: 'YOUR_SLACK_WEBHOOK_URL'
        channel: '#broadcast-alerts'
        title: '{{ .CommonAnnotations.summary }}'
        text: '{{ .CommonAnnotations.description }}'
        color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'

7. Alternatif İzleme Çözümleri

Prometheus + Grafana mükemmel olsa da, alternatifler şunlardır:

7.1 ELK Stack (Elasticsearch, Logstash, Kibana)

En İyisi: Log toplama, geçmiş olayları arama, uyumluluk audit trail’leri

Avantajları:

  • Log’lar için mükemmel (hatalar, uyarılar, config değişiklikleri)
  • Tam metin arama yetenekleri
  • Uzun vadeli depolama (yıllar) Prometheus’tan daha ucuz
  • Yerleşik makine öğrenimi (anomali tespiti)

Dezavantajları:

  • Metrikler için tasarlanmamış (Prometheus daha iyi)
  • Kurmak daha karmaşık
  • Daha yüksek kaynak gereksinimleri

Örnek Kullanım Durumu: Uyumluluk için tüm cihaz loglarını (syslog, uygulama logları) depolama, olaylar sırasında hataları arama

7.2 InfluxDB + Telegraf + Chronograf

En İyisi: Prometheus’tan daha yüksek kardinaliteye sahip zaman serisi verileri

Mimari:

1
ST 2110 Cihazları → Telegraf (agent) → InfluxDB → Chronograf/Grafana

Avantajları:

  • Özel olarak oluşturulmuş zaman serisi veritabanı
  • Daha iyi sıkıştırma (Prometheus’a karşı 4-10x)
  • Nanosaniye hassasiyeti için yerel destek (PTP için önemli)
  • Flux sorgu dili (PromQL’den daha güçlü)
  • Enterprise özellikleri: kümeleme, replikasyon

Dezavantajları:

  • Push tabanlı (tüm cihazlarda agent gerekli)
  • Enterprise sürümü pahalı
  • Prometheus’tan daha küçük topluluk

Ne Zaman Seçilmeli:

  • Nanosaniye hassasiyetli timestamp’lere ihtiyaç var
  • 1+ yıllık saniye düzeyinde metrikleri depolama
  • InfluxData ekosistemi zaten kullanılıyor

7.3 Zabbix

En İyisi: Geleneksel BT izleme, SNMP-ağırlıklı ortamlar

Avantajları:

  • Kapsamlı agent (OS, ağ, uygulamalar)
  • Yerleşik SNMP desteği
  • Cihazların otomatik keşfi
  • Olgun alert’leme (bağımlılıklar, yükseltmeler)

Dezavantajları:

  • Daha az modern UI
  • Cloud-native değil
  • Daha zayıf zaman serisi analizi

Ne Zaman Seçilmeli: Büyük SDI’dan IP’ye geçiş, legacy + IP için birleşik izleme gerekli

7.4 Ticari Çözümler

Tektronix Sentry

  • Amaç: Profesyonel broadcast video izleme
  • Özellikler: ST 2110 paket analizi, video kalite metrikleri (PSNR, SSIM), thumbnail önizlemeleri, SMPTE 2022-7 analizi
  • Fiyatlandırma: Cihaz başına $10K-$50K
  • Ne Zaman Seçilmeli: Video kalite metrikleri, düzenleyici uyumluluk gerekli

Grass Valley iControl

  • Amaç: Broadcast tesis yönetimi
  • Özellikler: Cihaz kontrolü, routing, izleme, otomasyon
  • Fiyatlandırma: Enterprise (satış ile iletişim)
  • Ne Zaman Seçilmeli: Büyük tesis, entegre kontrol + izleme gerekli

Phabrix Qx Series

  • Amaç: Taşınabilir ST 2110 analizörü
  • Özellikler: El tipi cihaz, dalga formu ekranı, göz paterni, PTP analizi
  • Fiyatlandırma: $5K-$15K
  • Ne Zaman Seçilmeli: Saha sorun giderme, devreye alma

7.5 Karşılaştırma Matrisi

Çözüm Kurulum Karmaşıklığı Maliyet Ölçeklenebilirlik Video-Özel En İyi Kullanım Durumu
Prometheus + Grafana Orta Ücretsiz Mükemmel ❌ (DIY exporter’lar) Genel ST 2110 metrikleri
ELK Stack Yüksek Ücretsiz/$$ Mükemmel Log toplama
InfluxDB Düşük Ücretsiz/$$$$ Mükemmel Yüksek hassasiyetli metrikler
Zabbix Orta Ücretsiz İyi Geleneksel BT
Tektronix Sentry Düşük $$$$$ Sınırlı Video kalitesi
Grass Valley iControl Yüksek $$$$$ Mükemmel Enterprise tesis


8. İleri Düzey İzleme: Video Kalitesi, Multicast ve Kapasite Planlaması

8.1 Video Kalite Metrikleri (TR-03 Uyumluluğu)

Paket kaybının ötesinde, SMPTE ST 2110-21 (Traffic Shaping ve Delivery Timing) uyarınca video zamanlama uyumluluğunu izlememiz gerekiyor.

TR-03 Zamanlama Modeli

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// video/tr03.go
package video

import (
    "math"
    "time"
)

type TR03Metrics struct {
    // Zamanlama modeli parametreleri
    GappedMode          bool      // true = gapped, false = linear
    TRODefaultNS        int64     // Varsayılan offset (1080p60 için 43.2ms)
    VRXFullNS           int64     // Tam buffer boyutu (tipik olarak 40ms)
    
    // Uyumluluk ölçümleri
    CInst               float64   // Anlık uyumluluk (0-1)
    CVMean              float64   // Pencere üzerinden ortalama uyumluluk
    VRXBufferLevel      int64     // Mevcut buffer doluluğu (nanosaniye)
    VRXBufferUnderruns  uint64    // Buffer underrun sayısı
    VRXBufferOverruns   uint64    // Buffer overrun sayısı
    
    // Türetilmiş metrikler
    TRSCompliant        bool      // Genel uyumluluk durumu
    LastViolation       time.Time
    ViolationCount      uint64
}

// C_INST hesapla (anlık uyumluluk)
// ST 2110-21'e göre: C_INST = (VRX_CURRENT - VRX_MIN) / (VRX_FULL - VRX_MIN)
func (m *TR03Metrics) CalculateCInst(vrxCurrent, vrxMin, vrxFull int64) float64 {
    if vrxFull == vrxMin {
        return 1.0
    }
    cInst := float64(vrxCurrent-vrxMin) / float64(vrxFull-vrxMin)
    
    // [0, 1] aralığına sabitle
    if cInst < 0 {
        cInst = 0
        m.VRXBufferUnderruns++
    } else if cInst > 1 {
        cInst = 1
        m.VRXBufferOverruns++
    }
    
    m.CInst = cInst
    return cInst
}

// C_V_MEAN hesapla (1 saniye üzerinden ortalama uyumluluk)
func (m *TR03Metrics) CalculateCVMean(cInstSamples []float64) float64 {
    if len(cInstSamples) == 0 {
        return 0
    }
    
    sum := 0.0
    for _, c := range cInstSamples {
        sum += c
    }
    
    m.CVMean = sum / float64(len(cInstSamples))
    return m.CVMean
}

// TR-03 uyumluluğunu kontrol et
// Uyumlu: C_V_MEAN >= 0.5 (buffer ortalama en az %50 dolu)
func (m *TR03Metrics) CheckCompliance() bool {
    compliant := m.CVMean >= 0.5 && m.VRXBufferUnderruns == 0
    
    if !compliant && m.TRSCompliant {
        m.LastViolation = time.Now()
        m.ViolationCount++
    }
    
    m.TRSCompliant = compliant
    return compliant
}

// TR-03 metrikleri için Prometheus exporter
type TR03Exporter struct {
    cInst            *prometheus.GaugeVec
    cVMean           *prometheus.GaugeVec
    bufferLevel      *prometheus.GaugeVec
    bufferUnderruns  *prometheus.CounterVec
    bufferOverruns   *prometheus.CounterVec
    trsCompliance    *prometheus.GaugeVec
}

func NewTR03Exporter() *TR03Exporter {
    return &TR03Exporter{
        cInst: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_tr03_c_inst",
                Help: "Instantaneous compliance metric (0-1)",
            },
            []string{"stream_id", "receiver"},
        ),
        cVMean: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_tr03_c_v_mean",
                Help: "Mean compliance over 1 second window (0-1)",
            },
            []string{"stream_id", "receiver"},
        ),
        bufferLevel: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_vrx_buffer_level_microseconds",
                Help: "Current VRX buffer fill level in microseconds",
            },
            []string{"stream_id", "receiver"},
        ),
        bufferUnderruns: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "st2110_vrx_buffer_underruns_total",
                Help: "Total VRX buffer underruns (frame drops)",
            },
            []string{"stream_id", "receiver"},
        ),
        bufferOverruns: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "st2110_vrx_buffer_overruns_total",
                Help: "Total VRX buffer overruns (excessive latency)",
            },
            []string{"stream_id", "receiver"},
        ),
        trsCompliance: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_trs_compliant",
                Help: "TR-03 compliance status (1=compliant, 0=violation)",
            },
            []string{"stream_id", "receiver"},
        ),
    }
}

TR-03 Alert Kuralları

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# alerts/tr03.yml
groups:
  - name: st2110_video_quality
    interval: 1s
    rules:
      # Buffer tükenmesi = frame drop
      - alert: ST2110BufferUnderrun
        expr: increase(st2110_vrx_buffer_underruns_total[10s]) > 0
        for: 0s  # Anında
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde buffer underrun"
          description: "VRX buffer underrun tespit edildi - frame'ler drop ediliyor!"
      
      # Düşük uyumluluk skoru
      - alert: ST2110LowCompliance
        expr: st2110_tr03_c_v_mean < 0.5
        for: 5s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde düşük TR-03 uyumluluğu"
          description: "C_V_MEAN = {{ $value }} (eşik: 0.5)"
      
      # Kritik: buffer neredeyse boş
      - alert: ST2110BufferCriticallyLow
        expr: st2110_vrx_buffer_level_microseconds < 10000
        for: 1s
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.stream_id }} üzerinde VRX buffer kritik seviyede düşük"
          description: "Buffer {{ $value }}μs'de (< 10ms) - tükenme yakın!"

8.2 Multicast-Özel İzleme

IGMP ve multicast routing, ST 2110 için kritiktir - bir hatalı yapılandırma her şeyi bozabilir.

IGMP Metrics Exporter

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// igmp/exporter.go
package igmp

import (
    "bufio"
    "fmt"
    "net"
    "os"
    "strings"
    "time"
    "github.com/prometheus/client_golang/prometheus"
)

type IGMPMetrics struct {
    // Interface/VLAN başına istatistikler
    ActiveGroupsPerVLAN map[string]int
    
    // Join/Leave zamanlaması
    LastJoinLatency     time.Duration
    LastLeaveLatency    time.Duration
    
    // IGMP querier durumu
    QuerierPresent      bool
    QuerierAddress      string
    LastQueryTime       time.Time
    
    // Bilinmeyen multicast (taşma)
    UnknownMulticastPPS uint64
    UnknownMulticastBPS uint64
    
    // IGMP mesaj sayaçları
    IGMPQueriesRx       uint64
    IGMPReportsT        uint64
    IGMPLeavesRx        uint64
    IGMPV2ReportsRx     uint64
    IGMPV3ReportsRx     uint64
}

type IGMPExporter struct {
    activeGroups        *prometheus.GaugeVec
    joinLatency         *prometheus.GaugeVec
    querierPresent      *prometheus.GaugeVec
    unknownMulticastPPS *prometheus.GaugeVec
    igmpQueries         *prometheus.CounterVec
    igmpReports         *prometheus.CounterVec
}

func NewIGMPExporter() *IGMPExporter {
    return &IGMPExporter{
        activeGroups: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_igmp_active_groups",
                Help: "Number of active IGMP multicast groups",
            },
            []string{"vlan", "interface"},
        ),
        joinLatency: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_igmp_join_latency_microseconds",
                Help: "Time to join multicast group in microseconds",
            },
            []string{"multicast_group"},
        ),
        querierPresent: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_igmp_querier_present",
                Help: "IGMP querier present on VLAN (1=yes, 0=no)",
            },
            []string{"vlan"},
        ),
        unknownMulticastPPS: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "st2110_unknown_multicast_pps",
                Help: "Unknown multicast packets per second (flooding)",
            },
            []string{"switch", "vlan"},
        ),
        igmpQueries: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "st2110_igmp_queries_total",
                Help: "Total IGMP query messages received",
            },
            []string{"vlan"},
        ),
        igmpReports: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "st2110_igmp_reports_total",
                Help: "Total IGMP report messages sent",
            },
            []string{"vlan", "version"},
        ),
    }
}

// Aktif grupları almak için /proc/net/igmp'yi parse et
func (e *IGMPExporter) CollectIGMPGroups() error {
    file, err := os.Open("/proc/net/igmp")
    if err != nil {
        return err
    }
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    currentIface := ""
    groupCount := 0
    
    for scanner.Scan() {
        line := scanner.Text()
        
        // Interface satırı: "1: eth0: ..."
        if strings.Contains(line, ":") && !strings.HasPrefix(line, " ") {
            if currentIface != "" {
                e.activeGroups.WithLabelValues("default", currentIface).Set(float64(groupCount))
            }
            parts := strings.Fields(line)
            if len(parts) >= 2 {
                currentIface = strings.TrimSuffix(parts[1], ":")
                groupCount = 0
            }
        }
        
        // Grup satırı: "  010100E0 1 0 00000000 0"
        if strings.HasPrefix(line, " ") && strings.TrimSpace(line) != "" {
            groupCount++
        }
    }
    
    if currentIface != "" {
        e.activeGroups.WithLabelValues("default", currentIface).Set(float64(groupCount))
    }
    
    return scanner.Err()
}

// IGMP join gecikmesini ölç
func (e *IGMPExporter) MeasureJoinLatency(multicastAddr string, ifaceName string) (time.Duration, error) {
    // Multicast adresini parse et
    maddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:0", multicastAddr))
    if err != nil {
        return 0, err
    }
    
    // Interface'i al
    iface, err := net.InterfaceByName(ifaceName)
    if err != nil {
        return 0, err
    }
    
    // Multicast grubuna katıl ve zamanı ölç
    start := time.Now()
    
    conn, err := net.ListenMulticastUDP("udp", iface, maddr)
    if err != nil {
        return 0, err
    }
    defer conn.Close()
    
    // İlk paketi bekle (başarılı join'i gösterir)
    conn.SetReadDeadline(time.Now().Add(5 * time.Second))
    buf := make([]byte, 1500)
    _, _, err = conn.ReadFromUDP(buf)
    
    latency := time.Since(start)
    
    if err == nil {
        e.joinLatency.WithLabelValues(multicastAddr).Set(float64(latency.Microseconds()))
    }
    
    return latency, err
}

// IGMP querier'ı kontrol et
func (e *IGMPExporter) CheckQuerier(vlan string) bool {
    // Bu, gNMI aracılığıyla switch'i sorgular
    // Şimdilik simüle et:
    // show ip igmp snooping querier vlan 100
    
    querierPresent := true  // Placeholder
    
    if querierPresent {
        e.querierPresent.WithLabelValues(vlan).Set(1)
    } else {
        e.querierPresent.WithLabelValues(vlan).Set(0)
    }
    
    return querierPresent
}

Kritik Multicast Eşikleri

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const (
    // IGMP join < 1 saniyede tamamlanmalı
    MaxIGMPJoinLatencyMS = 1000
    
    // Bilinmeyen multicast taşması eşiği
    // > 1000 pps ise, muhtemelen hatalı yapılandırma
    MaxUnknownMulticastPPS = 1000
    
    // IGMP querier mevcut olmalı
    // Querier olmadan, gruplar 260s sonra timeout olur
    RequireIGMPQuerier = true
)

Multicast Alert Kuralları

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# alerts/multicast.yml
groups:
  - name: st2110_multicast
    interval: 5s
    rules:
      # IGMP querier yok = 260s sonra felaket
      - alert: ST2110NoIGMPQuerier
        expr: st2110_igmp_querier_present == 0
        for: 10s
        labels:
          severity: critical
        annotations:
          summary: "VLAN {{ $labels.vlan }} üzerinde IGMP querier yok"
          description: "Querier olmadan IGMP grupları 260 saniye içinde timeout olacak!"
      
      # Bilinmeyen multicast taşması
      - alert: ST2110UnknownMulticastFlooding
        expr: st2110_unknown_multicast_pps > 1000
        for: 30s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.switch }} üzerinde bilinmeyen multicast taşması"
          description: "{{ $value }} pps bilinmeyen multicast (muhtemelen hatalı ayarlanmış kaynak)"
      
      # Yavaş IGMP join
      - alert: ST2110SlowIGMPJoin
        expr: st2110_igmp_join_latency_microseconds > 1000000
        for: 0s
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.multicast_group }} için yavaş IGMP join"
          description: "Join gecikmesi: {{ $value }}μs (> 1 saniye)"
      
      # Çok fazla multicast grubu (kapasite sorunu)
      - alert: ST2110TooManyMulticastGroups
        expr: st2110_igmp_active_groups > 1000
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.vlan }} üzerinde yüksek multicast grup sayısı"
          description: "{{ $value }} grup (switch TCAM tükenmiş olabilir)"

8.3 Kapasite Planlaması ve Tahminleme

Bant genişliğinizin veya portlarınızın ne zaman biteceğini tahmin edin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 4 hafta sonrası bant genişliği kullanımını tahmin et
predict_linear(
    sum(st2110_rtp_bitrate_bps)[1w:],
    4 * 7 * 24 * 3600  # 4 hafta (saniye cinsinden)
) / 100e9 * 100  # 100Gbps linkin yüzdesi

# Örnek sonuç: %92 (yakında yükseltme gerekli!)

# %100'e ne zaman ulaşılacağını tahmin et (zaman serisi kesişimi)
(100e9 - sum(st2110_rtp_bitrate_bps)) / 
  deriv(sum(st2110_rtp_bitrate_bps)[1w:])  # Dolana kadar saniye

# Kapasite planlama alert'i
- alert: ST2110CapacityExhausted
  expr: |
    predict_linear(sum(st2110_rtp_bitrate_bps)[1w:], 2*7*24*3600) / 100e9 > 0.9
  labels:
    severity: warning
    team: capacity-planning
  annotations:
    summary: "Bant genişliği kapasitesi < 2 haftada tükenecek"
    description: "Mevcut trend: 2 haftada {{ $value }}% kullanım"

Kapasite Planlama Dashboard’u

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "dashboard": {
    "title": "ST 2110 Capacity Planning",
    "panels": [
      {
        "title": "Bandwidth Growth Trend",
        "targets": [{
          "expr": "sum(st2110_rtp_bitrate_bps)",
          "legendFormat": "Current Bandwidth"
        }, {
          "expr": "predict_linear(sum(st2110_rtp_bitrate_bps)[1w:], 4*7*24*3600)",
          "legendFormat": "Predicted (4 weeks)"
        }]
      },
      {
        "title": "Days Until 90% Capacity",
        "targets": [{
          "expr": "(0.9 * 100e9 - sum(st2110_rtp_bitrate_bps)) / deriv(sum(st2110_rtp_bitrate_bps)[1w:]) / 86400"
        }],
        "format": "days"
      },
      {
        "title": "Stream Count Growth",
        "targets": [{
          "expr": "count(st2110_rtp_packets_received_total)"
        }]
      }
    ]
  }
}

8.4 Maliyet Analizi ve ROI

Yatırım Dökümü

Çözüm İlk Maliyet Yıllık Maliyet Personel Kesinti Tespiti
Açık Kaynak (Prometheus/Grafana/gNMI) $0 (yazılım) $5K (ops) 0.5 FTE < 5 saniye
InfluxDB Enterprise $20K (lisanslar) $10K (destek) 0.3 FTE < 5 saniye
Tektronix Sentry $50K (cihaz) $10K (destek) 0.2 FTE < 1 saniye
Grass Valley iControl $200K+ (tesis) $40K (destek) 1 FTE < 1 saniye
İzleme Yok $0 $0 2 FTE (yangın söndürme) 5-60 dakika

Kesinti Maliyeti Hesaplaması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Büyük Yayıncı (7/24 haber kanalı):
  Gelir: $100M/yıl = $11,416/saat
  İtibar hasarı: Olay başına $50K
  Düzenleyici cezalar: FCC ihlali başına $25K
  
Tek 1 saatlik kesinti maliyeti: $186K
  = $11K (kayıp gelir)
  + $50K (itibar)
  + $25K (düzenleyici)
  + $100K (acil destek, telafi prodüksiyonu)

ROI Hesaplaması:
  Açık Kaynak Stack Maliyeti: $5K/yıl
  Önlenen Kesintiler: 2/yıl (muhafazakar)
  Tasarruf: 2 × $186K = $372K
  ROI: ($372K - $5K) / $5K = 7,340%
  
  Geri Ödeme Süresi: < 1 hafta

Gerçek Dünya Olay Maliyeti

Vaka Çalışması: Büyük spor yayıncısı, 2023

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Olay: Canlı maç sırasında 45 dakikalık stream kesintisi
Kök Neden: Buffer tükenmelerine neden olan PTP kayması
Tespit Süresi: 12 dakika (izleyici şikayetleri)
Çözüm Süresi: 33 dakika (manuel failover)

Maliyetler:
- Kayıp reklam geliri: $450K
- Telafi yayın süresi: $80K
- Acil teknik destek: $15K
- İtibar hasarı (tahmini): $200K
Toplam: $745K

İzleme İle:
- Tespit süresi: 5 saniye (otomatik alert)
- Otomatik failover: 3 saniye
- Toplam kesinti: 8 saniye
- İzleyici etkisi: Minimal (tek frame drop)
- Maliyet: $0

Önlemek İçin Yatırım:
- Prometheus + Grafana + Custom Exporter'lar: $5K/yıl
- ROI: $745K kaybı önledi = %14,800 ROI

9. Production En İyi Pratikleri

9.1 Production için Güvenlik Sıkılaştırması

Güvenlik opsiyonel DEĞİLDİR - monitoring sistemlerinin tüm ağınıza erişimi vardır!

Ağ Segmentasyonu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Önerilen ağ mimarisi
networks:
  production_video:  # ST 2110 stream'leri (VLAN 100)
    subnet: 10.1.100.0/24
    access: monitoring için read-only
    
  monitoring:        # Prometheus/Grafana (VLAN 200)
    subnet: 10.1.200.0/24
    access: sadece yönetim
    
  management:        # Switch/device management (VLAN 10)
    subnet: 10.1.10.0/24
    access: kısıtlı (sadece monitoring exporter'ları)

firewall_rules:
  # Monitoring scrape'lerine izin ver
  - from: 10.1.200.0/24  # Prometheus
    to: 10.1.100.0/24    # Exporter'lar
    ports: [9100, 9200, 9273]
    protocol: TCP
    
  # Geri kalan her şeyi engelle
  - from: 10.1.100.0/24
    to: 10.1.200.0/24
    action: DENY

HashiCorp Vault ile Secrets Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// security/vault.go
package security

import (
    "fmt"
    vault "github.com/hashicorp/vault/api"
)

type SecretsManager struct {
    client *vault.Client
}

func NewSecretsManager(vaultAddr, token string) (*SecretsManager, error) {
    config := vault.DefaultConfig()
    config.Address = vaultAddr
    
    client, err := vault.NewClient(config)
    if err != nil {
        return nil, err
    }
    
    client.SetToken(token)
    
    return &SecretsManager{client: client}, nil
}

// Vault'tan gNMI kimlik bilgilerini al (environment variable'lardan değil!)
func (sm *SecretsManager) GetGNMICredentials(switchName string) (string, string, error) {
    path := fmt.Sprintf("secret/data/monitoring/gnmi/%s", switchName)
    
    secret, err := sm.client.Logical().Read(path)
    if err != nil {
        return "", "", err
    }
    
    if secret == nil {
        return "", "", fmt.Errorf("no credentials found for %s", switchName)
    }
    
    data := secret.Data["data"].(map[string]interface{})
    username := data["username"].(string)
    password := data["password"].(string)
    
    return username, password, nil
}

// Kimlik bilgilerini otomatik rotate et
func (sm *SecretsManager) RotateGNMIPassword(switchName string) error {
    // Yeni şifre oluştur
    newPassword := generateSecurePassword(32)
    
    // Switch'te güncelle (gNMI ile)
    if err := updateSwitchPassword(switchName, newPassword); err != nil {
        return err
    }
    
    // Vault'ta sakla
    path := fmt.Sprintf("secret/data/monitoring/gnmi/%s", switchName)
    data := map[string]interface{}{
        "data": map[string]interface{}{
            "username": "prometheus",
            "password": newPassword,
            "rotated_at": time.Now().Unix(),
        },
    }
    
    _, err := sm.client.Logical().Write(path, data)
    return err
}

Grafana RBAC (Role-Based Access Control)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# grafana/provisioning/access-control/roles.yaml
apiVersion: 1

roles:
  # Operatörler için read-only (görüntüleyebilir, değiştiremez)
  - name: "Broadcast Operator"
    description: "Dashboard'ları görüntüle ve alert'leri onayla"
    version: 1
    permissions:
      - action: "dashboards:read"
        scope: "dashboards:*"
      - action: "datasources:query"
        scope: "datasources:*"
      - action: "alerting:read"
        scope: "alert.rules:*"
      # Alert'leri onaylayabilir ama susturamaz
      - action: "alerting:write"
        scope: "alert.instances:*"
  
  # Mühendisler dashboard'ları düzenleyebilir
  - name: "Broadcast Engineer"
    description: "Dashboard'lar ve alert'ler oluştur/düzenle"
    version: 1
    permissions:
      - action: "dashboards:*"
        scope: "dashboards:*"
      - action: "alert.rules:*"
        scope: "alert.rules:*"
      - action: "datasources:query"
        scope: "datasources:*"
  
  # Sadece adminler
  - name: "Monitoring Admin"
    description: "Kullanıcı yönetimi dahil tam erişim"
    version: 1
    permissions:
      - action: "*"
        scope: "*"

# Kullanıcıları rollere eşle
user_roles:
  - email: "operator@company.com"
    role: "Broadcast Operator"
  - email: "engineer@company.com"
    role: "Broadcast Engineer"
  - email: "admin@company.com"
    role: "Monitoring Admin"

Tüm İletişim için TLS/mTLS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# TLS client sertifikalarıyla Prometheus
global:
  scrape_interval: 1s

scrape_configs:
  - job_name: 'st2110_streams'
    scheme: https
    tls_config:
      ca_file: /etc/prometheus/certs/ca.crt
      cert_file: /etc/prometheus/certs/client.crt
      key_file: /etc/prometheus/certs/client.key
      # Exporter sertifikalarını doğrula
      insecure_skip_verify: false
    static_configs:
      - targets: ['receiver-1:9100']

Sertifika Oluşturma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/bin/bash
# generate-certs.sh - CA ve client/server sertifikaları oluştur

# CA oluştur
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/C=US/ST=State/L=City/O=Broadcast/CN=ST2110-Monitoring-CA"

# Server sertifikası oluştur (exporter'lar için)
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
  -subj "/C=US/ST=State/L=City/O=Broadcast/CN=receiver-1"

# CA ile imzala
openssl x509 -req -days 365 -in server.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt

# Client sertifikası oluştur (Prometheus için)
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
  -subj "/C=US/ST=State/L=City/O=Broadcast/CN=prometheus"

openssl x509 -req -days 365 -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt

echo "✅ Sertifikalar oluşturuldu"

Uyumluluk için Audit Logging

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// audit/logger.go (gelişmiş)
package audit

import (
    "context"
    "encoding/json"
    "time"
)

type AuditEvent struct {
    Timestamp   time.Time             `json:"@timestamp"`
    EventType   string                `json:"event_type"`
    User        string                `json:"user"`
    UserIP      string                `json:"user_ip"`
    Action      string                `json:"action"`
    Resource    string                `json:"resource"`
    Result      string                `json:"result"`  // "success" veya "failure"
    Changes     map[string]interface{} `json:"changes,omitempty"`
    Severity    string                `json:"severity"`
}

type AuditLogger struct {
    esClient *elasticsearch.Client
    index    string
}

func (al *AuditLogger) LogConfigChange(ctx context.Context, event AuditEvent) error {
    event.Timestamp = time.Now()
    event.EventType = "config_change"
    
    data, _ := json.Marshal(event)
    
    _, err := al.esClient.Index(
        al.index,
        bytes.NewReader(data),
        al.esClient.Index.WithContext(ctx),
    )
    
    return err
}

// Grafana webhook ile kullanım
func GrafanaAuditWebhook(w http.ResponseWriter, r *http.Request) {
    var event struct {
        DashboardID int    `json:"dashboardId"`
        UserLogin   string `json:"userLogin"`
        Action      string `json:"action"`
    }
    
    json.NewDecoder(r.Body).Decode(&event)
    
    auditLogger.LogConfigChange(context.Background(), AuditEvent{
        User:     event.UserLogin,
        Action:   event.Action,
        Resource: fmt.Sprintf("dashboard:%d", event.DashboardID),
        Result:   "success",
        Severity: "info",
    })
}

Düzenleyici Uyumluluk için Retention:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# elasticsearch.yml
# FCC/Ofcom için 7 yıl log saklama
indices.lifecycle.rollover:
  - policy: audit-logs
    max_age: 7d
    max_size: 50gb

retention:
  min_age: 7y  # 7 yıl
  delete_phase: true

Rate Limiting ve DDoS Koruması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Grafana önünde nginx reverse proxy
http {
    limit_req_zone $binary_remote_addr zone=grafana:10m rate=10r/s;
    
    server {
        listen 443 ssl;
        server_name grafana.company.com;
        
        ssl_certificate /etc/nginx/certs/grafana.crt;
        ssl_certificate_key /etc/nginx/certs/grafana.key;
        
        # Rate limiting
        limit_req zone=grafana burst=20 nodelay;
        
        # Şüpheli patternleri engelle
        if ($http_user_agent ~* (bot|crawler|scanner)) {
            return 403;
        }
        
        location / {
            proxy_pass http://grafana:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}

9.2 Kubernetes Deployment

Modern altyapı için Kubernetes’te dağıtın:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# k8s/st2110-monitoring-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: st2110-monitoring
  labels:
    name: st2110-monitoring
    security: high
---
# Prometheus deployment
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: prometheus
  namespace: st2110-monitoring
spec:
  serviceName: prometheus
  replicas: 2  # HA
  selector:
    matchLabels:
      app: prometheus
  template:
    metadata:
      labels:
        app: prometheus
    spec:
      serviceAccountName: prometheus
      securityContext:
        runAsUser: 65534
        runAsNonRoot: true
        fsGroup: 65534
      containers:
      - name: prometheus
        image: prom/prometheus:latest
        args:
          - '--config.file=/etc/prometheus/prometheus.yml'
          - '--storage.tsdb.path=/prometheus'
          - '--storage.tsdb.retention.time=90d'
          - '--web.enable-lifecycle'
        ports:
        - containerPort: 9090
          name: http
        resources:
          requests:
            cpu: 2000m
            memory: 8Gi
          limits:
            cpu: 4000m
            memory: 16Gi
        volumeMounts:
        - name: config
          mountPath: /etc/prometheus
        - name: storage
          mountPath: /prometheus
        livenessProbe:
          httpGet:
            path: /-/healthy
            port: 9090
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /-/ready
            port: 9090
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: config
        configMap:
          name: prometheus-config
  volumeClaimTemplates:
  - metadata:
      name: storage
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 500Gi  # 90 günlük metrik
---
# Grafana deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana
  namespace: st2110-monitoring
spec:
  replicas: 2
  selector:
    matchLabels:
      app: grafana
  template:
    metadata:
      labels:
        app: grafana
    spec:
      containers:
      - name: grafana
        image: grafana/grafana:latest
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: GF_SECURITY_ADMIN_PASSWORD
          valueFrom:
            secretKeyRef:
              name: grafana-secrets
              key: admin-password
        - name: GF_DATABASE_TYPE
          value: postgres
        - name: GF_DATABASE_HOST
          value: postgres:5432
        - name: GF_DATABASE_NAME
          value: grafana
        - name: GF_DATABASE_USER
          valueFrom:
            secretKeyRef:
              name: grafana-secrets
              key: db-username
        - name: GF_DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: grafana-secrets
              key: db-password
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 1000m
            memory: 2Gi
        volumeMounts:
        - name: dashboards
          mountPath: /var/lib/grafana/dashboards
      volumes:
      - name: dashboards
        configMap:
          name: grafana-dashboards
---
# Prometheus için Service
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: st2110-monitoring
spec:
  selector:
    app: prometheus
  ports:
  - port: 9090
    targetPort: 9090
  type: ClusterIP
---
# Grafana için Ingress (TLS ile)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
  namespace: st2110-monitoring
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/rate-limit: "10"
spec:
  tls:
  - hosts:
    - grafana.company.com
    secretName: grafana-tls
  rules:
  - host: grafana.company.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: grafana
            port:
              number: 3000

Kolay Dağıtım için Helm Chart:

1
2
3
4
5
6
7
8
9
# Helm ile kur
helm repo add st2110-monitoring https://charts.muratdemirci.dev/st2110
helm install st2110-monitoring st2110-monitoring/st2110-stack \
  --namespace st2110-monitoring \
  --create-namespace \
  --set prometheus.retention=90d \
  --set grafana.adminPassword=secure-password \
  --set ingress.enabled=true \
  --set ingress.hostname=grafana.company.com

9.3 High Availability

Sorun: Monitoring sistemi tek hata noktası

Çözüm: Redundant Prometheus + Alertmanager

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Prometheus Federation
# Merkezi Prometheus, bölgesel Prometheus instance'larından scrape eder
scrape_configs:
  - job_name: 'federate'
    scrape_interval: 15s
    honor_labels: true
    metrics_path: '/federate'
    params:
      'match[]':
        - '{job="st2110_streams"}'
    static_configs:
      - targets:
          - 'prometheus-region-1:9090'
          - 'prometheus-region-2:9090'

Alertmanager Clustering:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# alertmanager.yml
global:
  resolve_timeout: 5m

route:
  receiver: 'default'
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 12h

receivers:
  - name: 'default'
    slack_configs:
      - api_url: 'WEBHOOK_URL'
        channel: '#st2110-alerts'

# Cluster ayarları
cluster:
  listen-address: "0.0.0.0:9094"
  peers:
    - "alertmanager-1:9094"
    - "alertmanager-2:9094"
    - "alertmanager-3:9094"

9.4 Alert Fatigue Önleme

Kaçınılması Gereken Anti-Patternler:

1
2
3
4
5
6
7
8
❌ Her paket kaybı > %0'da alarm
✅ Paket kaybı > %0.001, 10 saniye sürekli olduğunda alarm

❌ PTP offset > 0ns'de alarm
✅ PTP offset > 10μs, 5 saniye sürekli olduğunda alarm

❌ Tüm alarmları herkese gönder
✅ Severity'ye göre yönlendir (critical → PagerDuty, warning → Slack)

9.5 Metric Retention Stratejisi

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Prometheus retention
storage:
  tsdb:
    retention.time: 90d  # 90 gün lokal tut
    
# Eski verileri downsample et
- source_labels: [__name__]
  regex: 'st2110_rtp.*'
  target_label: __keep__
  replacement: '30d'  # 30 gün tam çözünürlük
  
# Uzun vadeli depolamaya arşivle (S3, vb.)
remote_write:
  - url: 'http://thanos-receive:19291/api/v1/receive'

Thanos ile Uzun Vadeli Saklama:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# docker-compose.yml'ye ekle
  thanos-sidecar:
    image: quay.io/thanos/thanos:latest
    command:
      - 'sidecar'
      - '--tsdb.path=/prometheus'
      - '--prometheus.url=http://prometheus:9090'
      - '--objstore.config-file=/etc/thanos/bucket.yml'
    volumes:
      - prometheus_data:/prometheus
      - ./thanos/bucket.yml:/etc/thanos/bucket.yml

# bucket.yml (S3 backend)
type: S3
config:
  bucket: "st2110-metrics-archive"
  endpoint: "s3.amazonaws.com"
  region: "us-east-1"

9.6 CI/CD Pipeline

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# .github/workflows/deploy-monitoring.yml
name: Deploy ST 2110 Monitoring

on:
  push:
    branches: [main]
    paths:
      - 'prometheus/**'
      - 'grafana/**'
      - 'exporters/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Validate Prometheus Config
        run: |
          docker run --rm -v $(pwd)/prometheus:/etc/prometheus \
            prom/prometheus:latest \
            promtool check config /etc/prometheus/prometheus.yml          
      
      - name: Validate Alert Rules
        run: |
          docker run --rm -v $(pwd)/prometheus:/etc/prometheus \
            prom/prometheus:latest \
            promtool check rules /etc/prometheus/alerts/*.yml          
      
      - name: Test Exporters
        run: |
          cd exporters/rtp && go test ./...
          cd exporters/ptp && go test ./...
          cd exporters/gnmi && go test ./...          

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Build Exporter Images
        run: |
          docker build -t myregistry/st2110-rtp-exporter:${{ github.sha }} exporters/rtp
          docker build -t myregistry/st2110-ptp-exporter:${{ github.sha }} exporters/ptp
          docker build -t myregistry/st2110-gnmi-exporter:${{ github.sha }} exporters/gnmi          
      
      - name: Push to Registry
        run: |
          echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
          docker push myregistry/st2110-rtp-exporter:${{ github.sha }}
          docker push myregistry/st2110-ptp-exporter:${{ github.sha }}
          docker push myregistry/st2110-gnmi-exporter:${{ github.sha }}          

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Deploy to Kubernetes
        uses: azure/k8s-deploy@v1
        with:
          manifests: |
            k8s/namespace.yaml
            k8s/prometheus.yaml
            k8s/grafana.yaml            
          images: |
            myregistry/st2110-rtp-exporter:${{ github.sha }}
            myregistry/st2110-ptp-exporter:${{ github.sha }}
            myregistry/st2110-gnmi-exporter:${{ github.sha }}            
          kubectl-version: 'latest'
      
      - name: Verify Deployment
        run: |
          kubectl rollout status statefulset/prometheus -n st2110-monitoring
          kubectl rollout status deployment/grafana -n st2110-monitoring          
      
      - name: Run Smoke Tests
        run: |
          # Prometheus erişilebilir mi?
          kubectl run --rm -it --restart=Never curl --image=curlimages/curl -- \
            curl -f http://prometheus.st2110-monitoring:9090/-/healthy
          
          # Grafana erişilebilir mi?
          kubectl run --rm -it --restart=Never curl --image=curlimages/curl -- \
            curl -f http://grafana.st2110-monitoring:3000/api/health          

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Slack Notification
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'ST 2110 Monitoring deployment: ${{ job.status }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

10. Sorun Giderme Playbook’ları ve Gerçek Dünya Senaryoları

10.1 Olay Müdahale Çerçevesi

Her production ST 2110 tesisi, yaygın olaylar için yapılandırılmış playbook’lara ihtiyaç duyar.

Incident Response Flow

Temel İlkeler:

  1. Hız: Tespit < 5s, Müdahale < 3s (otomatik)
  2. Otomasyon: %80 olaylar otomatik çözülmeli
  3. Loglama: Her eylem kaydedilmeli (compliance)
  4. Öğrenme: Her olay sonrası analiz gerektirir

Playbook 1: Paket Kaybı Artışı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
trigger_condition: "Paket kaybı > %0.01, 30 saniye sürekli"
severity: critical
symptoms:
  - "Çıkışta görsel bozulmalar (bloklanma, pixelation)"
  - "Ses kesintileri veya tıklamalar"
  - "Prometheus uyarısı: ST2110HighPacketLoss"

investigation_steps:
  - step: 1
    action: "Etkilenen stream(leri) tanımla"
    command: "promtool query instant 'topk(10, st2110_rtp_packet_loss_rate)'"
  
  - step: 2
    action: "Kaynağa ağ yolunu kontrol et"
    command: "mtraced -s 239.1.1.10"
  
  - step: 3
    action: "Switch'lerde QoS yapılandırmasını doğrula"
    query: "st2110_switch_qos_dropped_packets{queue='video-priority'}"
  
  - step: 4
    action: "Switch buffer kullanımını analiz et"
    query: "st2110_switch_qos_buffer_utilization > 80"

automated_remediation:
  - condition: "Kayıp > %0.1, 10 saniye"
    action: "SMPTE 2022-7 failover tetikle"
    script: "/usr/local/bin/st2022-7-failover.sh {{ .stream_id }}"
  
  - condition: "Failover sonrası kayıp devam ediyor"
    action: "Trafiği yedek yoldan yönlendir"
    script: "/usr/local/bin/network-reroute.sh {{ .stream_id }}"

Playbook 2: PTP Senkronizasyon Kaybı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
trigger_condition: "PTP offset > 10μs VEYA clock state != LOCKED"
severity: critical
symptoms:
  - "Audio/video senkronizasyon kayması (lip sync sorunları)"
  - "Frame zamanlama hataları"
  - "Genlock başarısızlıkları"

investigation_steps:
  - step: 1
    action: "PTP grandmaster durumunu kontrol et"
    query: "st2110_ptp_grandmaster_id"
    expected: "Tek tutarlı grandmaster ID"
  
  - step: 2
    action: "Tüm cihazlarda PTP offset'i doğrula"
    query: "abs(st2110_ptp_offset_nanoseconds) > 1000"
  
  - step: 3
    action: "PTP topoloji değişikliklerini kontrol et"
    query: "changes(st2110_ptp_grandmaster_id[5m]) > 0"

automated_remediation:
  - condition: "Grandmaster erişilemez"
    action: "Yedek grandmaster'a failover"
    script: "/usr/local/bin/ptp-failover.sh"

9.2 Gerçek Dünya Sorun Giderme Örnekleri

Örnek 1: Aralıklı Bloklanma Gizemi

Belirti: Kamera 5’te rastgele pixelation, 2-3 dakikada bir

Araştırma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Adım 1: Paket kaybını kontrol et
st2110_rtp_packet_loss_rate{stream_id="cam5_vid"}
# Sonuç: %0.015 (eşiğin üzerinde!)

# Adım 2: Ağ metrikleriyle ilişkilendir
st2110_switch_interface_tx_bytes{interface=~"Ethernet5"}
# Sonuç: %95 kullanıma periyodik yükselmeler

# Adım 3: O port'ta başka ne var kontrol et
# Sonuç: Kamera 5 + Kamera 6 + preview feed = 10Gbps port'ta 25Gbps!

Kök Neden: Port oversubscription (2.5x!)

Çözüm: Kamera 6’yı farklı port’a taşı


10.3 Disaster Recovery ve Chaos Engineering

Aylık DR Tatbikatları

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# /etc/st2110/dr-drills.yaml
dr_drills:
  # Drill 1: Simulated Grandmaster Failure
  - name: "PTP Grandmaster Failure"
    frequency: monthly
    steps:
      - description: "Stop PTP grandmaster daemon"
        command: "systemctl stop ptp4l"
        target: "ptp-grandmaster-1"
        
      - description: "Monitor failover time to backup"
        query: "changes(st2110_ptp_grandmaster_id[5m])"
        expected: "< 5 seconds to lock to backup"
        
      - description: "Verify all devices locked to backup"
        query: "count(st2110_ptp_clock_state{state='LOCKED'})"
        expected: "All devices"
        
      - description: "Restore primary grandmaster"
        command: "systemctl start ptp4l"
        target: "ptp-grandmaster-1"
    
    success_criteria:
      - "Failover time < 5 seconds"
      - "No packet loss during failover"
      - "All devices re-lock to primary within 60 seconds"
  
  # Drill 2: Network Partition
  - name: "Network Partition (Split Brain)"
    frequency: monthly
    steps:
      - description: "Block multicast between core switches"
        command: "iptables -A FORWARD -d 239.0.0.0/8 -j DROP"
        target: "core-switch-1"
        
      - description: "Verify SMPTE 2022-7 seamless switching"
        query: "st2110_st2022_7_switching_events"
        expected: "Increment by 1 per stream"
        
      - description: "Verify no frame drops"
        query: "increase(st2110_vrx_buffer_underruns_total[1m])"
        expected: "0"
        
      - description: "Restore connectivity"
        command: "iptables -D FORWARD -d 239.0.0.0/8 -j DROP"
        target: "core-switch-1"
    
    success_criteria:
      - "All streams switch to backup path"
      - "Zero frame drops"
      - "Automatic return to primary"
  
  # Drill 3: Prometheus HA Failover
  - name: "Monitoring System Failure"
    frequency: quarterly
    steps:
      - description: "Kill primary Prometheus"
        command: "docker stop prometheus-primary"
        target: "monitoring-host-1"
        
      - description: "Verify alerts still firing"
        command: "curl http://alertmanager:9093/api/v2/alerts | jq '. | length'"
        expected: "> 0 (alerts preserved)"
        
      - description: "Verify Grafana switches to secondary"
        command: "curl http://grafana:3000/api/datasources | jq '.[] | select(.isDefault==true).name'"
        expected: "Prometheus-Secondary"
        
      - description: "Restore primary"
        command: "docker start prometheus-primary"
        target: "monitoring-host-1"
    
    success_criteria:
      - "Zero alert loss"
      - "Grafana dashboards remain functional"
      - "Primary syncs state on recovery"

Otomatik Chaos Testi

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// chaos/engine.go
package chaos

import (
    "fmt"
    "math/rand"
    "time"
)

type ChaosEngine struct {
    prometheus   *PrometheusClient
    alertmanager *AlertmanagerClient
}

func (c *ChaosEngine) RunWeeklyChaos() {
    experiments := []ChaosExperiment{
        {"inject_packet_loss", 0.5, 30 * time.Second},
        {"inject_jitter", 0.3, 60 * time.Second},
        {"kill_random_exporter", 0.2, 5 * time.Minute},
        {"ptp_offset_spike", 0.4, 15 * time.Second},
    }
    
    // Pick random experiment
    exp := experiments[rand.Intn(len(experiments))]
    
    log.Printf("🔥 CHAOS: Running %s for %v", exp.name, exp.duration)
    
    switch exp.name {
    case "inject_packet_loss":
        c.injectPacketLoss(exp.severity, exp.duration)
    case "inject_jitter":
        c.injectJitter(exp.severity, exp.duration)
    // ... etc
    }
    
    // Verify monitoring detected the issue
    if !c.verifyAlertFired(exp.name, exp.duration) {
        log.Printf("❌ CHAOS FAILURE: Alert did not fire for %s", exp.name)
        // Page on-call: monitoring system broken!
    } else {
        log.Printf("✅ CHAOS SUCCESS: Alert fired correctly for %s", exp.name)
    }
}

func (c *ChaosEngine) injectPacketLoss(severity float64, duration time.Duration) {
    // Use tc (traffic control) to drop packets
    dropRate := int(severity * 10)  // 0.5 -> 5%
    
    cmd := fmt.Sprintf(
        "tc qdisc add dev eth0 root netem loss %d%%",
        dropRate,
    )
    
    exec.Command("bash", "-c", cmd).Run()
    time.Sleep(duration)
    exec.Command("bash", "-c", "tc qdisc del dev eth0 root").Run()
}

10.5 Detaylı Grafana Dashboard Örnekleri

Problem: “Dashboard’lar nasıl görünmeli?” - İşte örnekler!

Dashboard 1: Stream Genel Bakış (Operasyonlar)

Amaç: İlk gördüğünüz şey - stream’ler iyi mi?

Layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌─────────────────────────────────────────────────┐
│ ST 2110 Facility Overview                      │
├─────────────┬─────────────┬─────────────────────┤
│ Critical    │ Active      │ Network             │
│ Alerts: 2   │ Streams: 48 │ Bandwidth: 85%      │
│ [RED]       │ [GREEN]     │ [YELLOW]            │
├─────────────┴─────────────┴─────────────────────┤
│ Packet Loss Heatmap (Last Hour)                │
│ ┌───────────────────────────────────────────┐  │
│ │ cam1  ████████████████░░░░░░░░░░░░░░░░    │  │
│ │ cam2  ████████████████████████████████    │  │
│ │ cam3  ████████████████████░░░░░░░░░░░░    │  │
│ │ ...                                       │  │
│ └───────────────────────────────────────────┘  │
│ Green = < 0.001% | Yellow = 0.001-0.01% | Red = > 0.01%
├─────────────────────────────────────────────────┤
│ PTP Offset Timeline (All Devices)              │
│ ┌───────────────────────────────────────────┐  │
│ │                                           │  │
│ │    ────────────────────────────────────   │  │
│ │  ↑ 10μs                                   │  │
│ │  │                                        │  │
│ │  ↓ -10μs                                  │  │
│ │    cam1 ──  cam2 ──  cam3 ──             │  │
│ └───────────────────────────────────────────┘  │
├─────────────────────────────────────────────────┤
│ Recent Events (Last 10)                         │
│ • 14:32:15 - High jitter on cam5 (1.2ms)       │
│ • 14:30:42 - Packet loss spike on cam2 (0.05%) │
│ • 14:28:10 - PTP offset cam7 recovered         │
└─────────────────────────────────────────────────┘

Key Panels:

  1. Stat Panels (Top Row): Critical alerts, active streams, network %
  2. Heatmap: Packet loss per stream (color-coded, easy to spot issues)
  3. Timeline: Tüm cihazlarda PTP offset (kayma desenlerini tespit et)
  4. Event Log: Recent alerts (with timestamps and stream IDs)

Dashboard 2: Stream Derinlemesine İnceleme (Sorun Giderme)

Amaç: Stream’de sorun olduğunda, burada teşhis edin

Layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌─────────────────────────────────────────────────┐
│ Stream: Camera 5 - Video [239.1.1.15:20000]    │
├─────────────────────────────────────────────────┤
│ Current Status: ⚠️ WARNING                      │
│ • Jitter: 1.2ms (threshold: 1.0ms)             │
│ • Packet Loss: 0.008% (OK)                     │
│ • PTP Offset: 850ns (OK)                       │
├──────────────────────┬──────────────────────────┤
│ Packet Loss (24h)    │ Jitter (24h)            │
│ [Graph]              │ [Graph]                 │
│                      │                         │
│ Avg: 0.003%          │ Avg: 650μs              │
│ Max: 0.05%           │ Max: 1.5ms              │
│ @14:30               │ @14:32                  │
├──────────────────────┴──────────────────────────┤
│ Network Path                                    │
│ Camera → [switch-1] → [core-1] → [core-2] → RX │
│          ↓ 30%        ↓ 85%      ↓ 45%          │
│          (utilization)                          │
├─────────────────────────────────────────────────┤
│ Correlated Metrics                              │
│ • Switch buffer: 75% (increasing)               │
│ • QoS drops: 0 (good)                           │
│ • IGMP groups: 48 (stable)                      │
├─────────────────────────────────────────────────┤
│ Logs (related to this stream)                   │
│ [Loki panel showing logs with "cam5" keyword]  │
└─────────────────────────────────────────────────┘

Key Features:

  1. Single-stream focus (selected via dropdown)
  2. All metrics for that stream in one view
  3. Network path visualization (where is bottleneck?)
  4. Log correlation (metrics + logs in same dashboard)

Dashboard 3: Ağ Sağlığı (Altyapı)

Amaç: Switch’leri izleyen ağ mühendisleri için

Layout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────┐
│ Network Infrastructure - ST 2110 VLANs          │
├─────────────────────────────────────────────────┤
│ Switch Port Utilization (All Core Switches)    │
│ ┌───────────────────────────────────────────┐  │
│ │ core-1/Et1  ████████████████████ 85%     │  │
│ │ core-1/Et2  ████████████░░░░░░░ 60%      │  │
│ │ core-2/Et1  ███████████████████░ 75%     │  │
│ │ core-2/Et2  ████████░░░░░░░░░░░ 40%      │  │
│ └───────────────────────────────────────────┘  │
├─────────────────────────────────────────────────┤
│ Multicast Bandwidth per VLAN                    │
│ [Stacked area chart]                            │
│ VLAN 100 (video)  ─────────────────────────     │
│ VLAN 101 (audio)  ─────                          │
│ VLAN 102 (anc)    ──                            │
├──────────────────────┬──────────────────────────┤
│ QoS Queue Drops      │ IGMP Group Count        │
│ [Graph per queue]    │ [Gauge]                 │
│ • video-priority: 0  │ 48 groups (expected 50) │
│ • best-effort: 1.2K  │                         │
├──────────────────────┴──────────────────────────┤
│ Switch Buffer Utilization                       │
│ [Heatmap: switch × interface]                   │
└─────────────────────────────────────────────────┘

10.6 Compliance ve Audit Logging

Düzenleyici uyumluluk için (FCC, Ofcom, vb.), tüm olayları loglayın:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// audit/logger.go
package audit

import (
    "encoding/json"
    "time"
    "github.com/elastic/go-elasticsearch/v8"
)

type AuditLog struct {
    Timestamp    time.Time `json:"@timestamp"`
    Event        string    `json:"event"`
    User         string    `json:"user"`
    Severity     string    `json:"severity"`
    StreamID     string    `json:"stream_id"`
    MetricValue  float64   `json:"metric_value"`
    ActionTaken  string    `json:"action_taken"`
    IncidentID   string    `json:"incident_id"`
}

type AuditLogger struct {
    esClient *elasticsearch.Client
}

func NewAuditLogger() (*AuditLogger, error) {
    es, err := elasticsearch.NewDefaultClient()
    if err != nil {
        return nil, err
    }
    
    return &AuditLogger{esClient: es}, nil
}

func (l *AuditLogger) LogIncident(log AuditLog) error {
    log.Timestamp = time.Now()
    
    data, err := json.Marshal(log)
    if err != nil {
        return err
    }
    
    // Store in Elasticsearch (7-year retention for compliance)
    _, err = l.esClient.Index(
        "st2110-audit-logs",
        bytes.NewReader(data),
    )
    
    return err
}

// Example usage
func onPacketLossAlert(streamID string, lossRate float64) {
    audit.LogIncident(AuditLog{
        Event:       "Packet loss threshold exceeded",
        Severity:    "critical",
        StreamID:    streamID,
        MetricValue: lossRate,
        ActionTaken: "Automatic failover to SMPTE 2022-7 backup stream",
        IncidentID:  generateIncidentID(),
    })
}

10. İleri Düzey Entegrasyonlar ve Performans Ayarı

10.1 NMOS Kontrol Düzlemi Sağlığının İzlenmesi

NMOS entegrasyonuna geçmeden önce, NMOS kontrol düzleminin kendisini izlememiz gerekiyor. NMOS çökerse, tüm tesis kontrolünü kaybedersiniz!

Gerçek Dünya Olayı:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Senaryo: Canlı prodüksiyon sırasında NMOS registry çökmesi
Zaman: Akşam haberleri sırasında 14:32

T+0s:    NMOS registry çöküyor (disk log'larla doldu)
T+30s:   Cihazlar heartbeat yanıtı almayı kesiyor
T+60s:   Node'lar registry'de "stale" olarak işaretleniyor
T+120s:  Operatörler stream bağlanamıyor/ayıramıyor (IS-05 başarısız)
T+180s:  Kamera operatörleri arıyor: "Kontrol sistemi yanıt vermiyor!"
T+600s:  Acil durum: Manuel SDI patch kullanıldı (IP tesisinin amacını yok etti!)

Kök Neden: Registry veritabanı izlenmiyor, disk log'larla doldu
Etki: 10 dakika manuel müdahale, uzaktan kontrol kaybı
Ders: Monitoring control plane'i izle!

İzlenecek NMOS Metrikleri:

  • IS-04 Registry Sağlığı: Availability, response time, last successful query
  • Node Registration: Active nodes, stale nodes (12+ saniye), expired nodes (5+ dakika)
  • Resources: Total senders, receivers, flows, devices, sources
  • IS-05 Connection Management: Active connections, failed connections, success rate
  • Resource Mismatches: Senders without flow, receivers not connected, orphaned flows

NMOS Alert Kuralları:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# alerts/nmos.yml
groups:
  - name: nmos_control_plane
    interval: 10s
    rules:
      # Registry çöktü = FELAKET
      - alert: NMOSRegistryDown
        expr: nmos_registry_available == 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "NMOS Registry ÇÖKTÜ"
          description: "ST 2110 kaynakları keşfedilemez veya kontrol edilemez!"
      
      # Çok sayıda stale node (ağ sorunu?)
      - alert: NMOSManyStaleNodes
        expr: nmos_stale_nodes > 5
        for: 30s
        labels:
          severity: warning
        annotations:
          summary: "{{ $value }} stale NMOS node'u"
          description: "Node'lar heartbeat göndermiyor - ağ sorunu?"
      
      # IS-05 bağlantı başarısızlıkları
      - alert: NMOSConnectionFailures
        expr: rate(nmos_failed_connections_total[5m]) > 0.1
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Yüksek IS-05 connection başarısızlığı"
          description: "Stream'ler bağlanamıyor - IS-05 problemi?"
      
      # Registry yanıt süresi
      - alert: NMOSRegistrySlow
        expr: nmos_query_duration_seconds{endpoint="nodes"} > 0.5
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "NMOS registry yavaş yanıt veriyor"
          description: "Query süresi {{ $value }}s (> 500ms)"

NMOS Metrics Implementation (Go):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// nmos/metrics.go
package nmos

import (
    "time"
    "github.com/prometheus/client_golang/prometheus"
)

type NMOSMetrics struct {
    // IS-04 Registry Health
    RegistryAvailable       bool
    RegistryResponseTimeMs  float64
    LastSuccessfulQuery     time.Time
    
    // Node Registration
    ActiveNodes             int
    StaleNodes              int      // 12+ saniye görülmeyen node'lar
    ExpiredNodes            int      // 5+ dakika görülmeyen node'lar
    NewNodesLast5Min        int
    
    // Resources
    TotalSenders            int
    TotalReceivers          int
    TotalFlows              int
    TotalDevices            int
    TotalSources            int
    
    // IS-05 Connection Management
    ActiveConnections       int
    FailedConnectionsTotal  uint64
    ConnectionAttempts      uint64
    ConnectionSuccessRate   float64
    
    // Resource Mismatches
    SendersWithoutFlow      int      // Sender var ama flow yok
    ReceiversNotConnected   int      // Receiver var ama sender yok
    FlowsWithoutSender      int      // Sahipsiz flow'lar
    
    // API Performance
    IS04QueryDurationMs     float64
    IS05ConnectionDurationMs float64
    WebSocketEventsPerSec   float64
    
    // Subscription Health
    ActiveSubscriptions     int
    FailedSubscriptions     uint64
}

// Eşik değerleri
const (
    MaxRegistryResponseMs   = 500    // 500ms maks yanıt
    MaxStaleNodeCount       = 5      // 5 stale node = sorun
    MinConnectionSuccessRate = 0.95  // %95 başarı oranı
    MaxNodeRegistrationAge  = 60     // 60s maks son heartbeat'ten bu yana
)

NMOS Health Checker Implementation:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// nmos/health_checker.go
package nmos

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
    "github.com/prometheus/client_golang/prometheus"
)

type NMOSHealthChecker struct {
    registryURL string
    metrics     NMOSMetrics
    
    // Prometheus exporter'lar
    registryAvailable    *prometheus.GaugeVec
    activeNodes          *prometheus.GaugeVec
    staleNodes           *prometheus.GaugeVec
    failedConnections    *prometheus.CounterVec
    queryDuration        *prometheus.HistogramVec
}

func NewNMOSHealthChecker(registryURL string) *NMOSHealthChecker {
    return &NMOSHealthChecker{
        registryURL: registryURL,
        
        registryAvailable: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "nmos_registry_available",
                Help: "NMOS registry availability (1=up, 0=down)",
            },
            []string{"registry"},
        ),
        
        activeNodes: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "nmos_active_nodes",
                Help: "Active NMOS node sayısı",
            },
            []string{"registry", "type"},  // type: device, sender, receiver
        ),
        
        staleNodes: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "nmos_stale_nodes",
                Help: "Stale NMOS node sayısı (12s'de heartbeat yok)",
            },
            []string{"registry"},
        ),
        
        failedConnections: prometheus.NewCounterVec(
            prometheus.CounterOpts{
                Name: "nmos_failed_connections_total",
                Help: "Toplam başarısız IS-05 connection denemesi",
            },
            []string{"registry", "reason"},
        ),
        
        queryDuration: prometheus.NewHistogramVec(
            prometheus.HistogramOpts{
                Name: "nmos_query_duration_seconds",
                Help: "NMOS query süresi (saniye)",
                Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1.0, 5.0},
            },
            []string{"registry", "endpoint"},
        ),
    }
}

func (nhc *NMOSHealthChecker) CheckHealth() error {
    // 1. Registry erişilebilirliğini kontrol et
    start := time.Now()
    resp, err := http.Get(nhc.registryURL + "/x-nmos/query/v1.3/nodes")
    duration := time.Since(start).Seconds()
    
    nhc.queryDuration.WithLabelValues(nhc.registryURL, "nodes").Observe(duration)
    
    if err != nil {
        nhc.registryAvailable.WithLabelValues(nhc.registryURL).Set(0)
        return fmt.Errorf("registry erişilemez: %v", err)
    }
    defer resp.Body.Close()
    
    nhc.registryAvailable.WithLabelValues(nhc.registryURL).Set(1)
    nhc.metrics.RegistryAvailable = true
    nhc.metrics.RegistryResponseTimeMs = duration * 1000
    
    // 2. Node'ları parse et
    var nodes []struct {
        ID      string    `json:"id"`
        Label   string    `json:"label"`
        Version string    `json:"version"`
        Self    struct {
            Href string `json:"href"`
        } `json:"self"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&nodes); err != nil {
        return fmt.Errorf("node'lar parse edilemedi: %v", err)
    }
    
    // 3. Stale node'ları say
    now := time.Now()
    staleCount := 0
    activeCount := len(nodes)
    
    for _, node := range nodes {
        // Her node için version timestamp'ini kontrol et
        // (Basitleştirilmiş - production'da tam heartbeat logic kullan)
        if duration > 12.0 {
            staleCount++
        }
    }
    
    nhc.activeNodes.WithLabelValues(nhc.registryURL, "all").Set(float64(activeCount))
    nhc.staleNodes.WithLabelValues(nhc.registryURL).Set(float64(staleCount))
    
    nhc.metrics.ActiveNodes = activeCount
    nhc.metrics.StaleNodes = staleCount
    
    return nil
}

// Sender/Receiver/Flow sayılarını topla
func (nhc *NMOSHealthChecker) CollectResourceCounts() error {
    endpoints := map[string]string{
        "senders":   "/x-nmos/query/v1.3/senders",
        "receivers": "/x-nmos/query/v1.3/receivers",
        "flows":     "/x-nmos/query/v1.3/flows",
        "devices":   "/x-nmos/query/v1.3/devices",
        "sources":   "/x-nmos/query/v1.3/sources",
    }
    
    for resourceType, path := range endpoints {
        start := time.Now()
        resp, err := http.Get(nhc.registryURL + path)
        duration := time.Since(start).Seconds()
        
        nhc.queryDuration.WithLabelValues(nhc.registryURL, resourceType).Observe(duration)
        
        if err != nil {
            continue
        }
        defer resp.Body.Close()
        
        var resources []map[string]interface{}
        json.NewDecoder(resp.Body).Decode(&resources)
        
        count := len(resources)
        
        switch resourceType {
        case "senders":
            nhc.metrics.TotalSenders = count
        case "receivers":
            nhc.metrics.TotalReceivers = count
        case "flows":
            nhc.metrics.TotalFlows = count
        case "devices":
            nhc.metrics.TotalDevices = count
        case "sources":
            nhc.metrics.TotalSources = count
        }
    }
    
    return nil
}

// Prometheus handler
func (nhc *NMOSHealthChecker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Health check yap
    nhc.CheckHealth()
    nhc.CollectResourceCounts()
    
    // Metrikleri expose et
    prometheus.Handler().ServeHTTP(w, r)
}

Kullanım:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// main.go
package main

import (
    "net/http"
    "time"
    "myproject/nmos"
)

func main() {
    // NMOS health checker başlat
    checker := nmos.NewNMOSHealthChecker("http://nmos-registry.local")
    
    // Periyodik kontroller
    go func() {
        ticker := time.NewTicker(10 * time.Second)
        for range ticker.C {
            if err := checker.CheckHealth(); err != nil {
                log.Printf("NMOS health check failed: %v", err)
            }
        }
    }()
    
    // Metrics endpoint
    http.Handle("/metrics", checker)
    http.ListenAndServe(":9300", nil)
}

10.2 NMOS Entegrasyonu: Stream’lerin Otomatik Keşfi

Artık NMOS sağlığını izlediğimize göre, onu otomatik keşif için kullanalım!

Faydalar:

  • Sıfır Yapılandırma: Stream’ler NMOS’tan otomatik keşfedilir
  • Dinamik: Yeni kameralar/kaynaklar otomatik izlenir
  • Tutarlı: Production kontrol sistemiyle aynı etiketler/ID’ler
  • Ölçeklenebilir: Config dosyalarına dokunmadan 100 stream ekle

NMOS Auto-Discovery Implementation:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// nmos/autodiscovery.go
package nmos

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

type AutoDiscovery struct {
    registryURL string
    streams     map[string]StreamConfig
    lastUpdate  time.Time
}

type StreamConfig struct {
    StreamID    string
    Label       string
    MulticastIP string
    Port        int
    Type        string  // "video", "audio", "ancillary"
    Format      string  // "1080p60", "48kHz/24bit", etc.
}

func NewAutoDiscovery(registryURL string) *AutoDiscovery {
    return &AutoDiscovery{
        registryURL: registryURL,
        streams:     make(map[string]StreamConfig),
    }
}

// NMOS'tan stream'leri keşfet
func (ad *AutoDiscovery) DiscoverStreams() ([]StreamConfig, error) {
    // 1. Tüm flow'ları al
    flows, err := ad.getFlows()
    if err != nil {
        return nil, err
    }
    
    // 2. Flow'ları sender'larla eşle
    senders, err := ad.getSenders()
    if err != nil {
        return nil, err
    }
    
    // 3. Stream yapılandırmaları oluştur
    var configs []StreamConfig
    
    for _, flow := range flows {
        // Bu flow için sender bul
        sender := ad.findSenderForFlow(flow.ID, senders)
        if sender == nil {
            continue
        }
        
        // Multicast IP ve port'u al
        transport := sender.Manifest.Transport
        if transport.Type != "urn:x-nmos:transport:rtp" {
            continue  // Sadece RTP stream'leri
        }
        
        config := StreamConfig{
            StreamID:    flow.ID,
            Label:       flow.Label,
            MulticastIP: transport.MulticastIP,
            Port:        transport.DestinationPort,
            Type:        ad.getStreamType(flow),
            Format:      flow.Format,
        }
        
        configs = append(configs, config)
        ad.streams[config.StreamID] = config
    }
    
    ad.lastUpdate = time.Now()
    
    fmt.Printf("Keşfedildi: %d stream\n", len(configs))
    return configs, nil
}

func (ad *AutoDiscovery) getFlows() ([]NMOSFlow, error) {
    url := fmt.Sprintf("%s/x-nmos/query/v1.3/flows", ad.registryURL)
    
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var flows []NMOSFlow
    if err := json.NewDecoder(resp.Body).Decode(&flows); err != nil {
        return nil, err
    }
    
    return flows, nil
}

func (ad *AutoDiscovery) getSenders() ([]NMOSSender, error) {
    url := fmt.Sprintf("%s/x-nmos/query/v1.3/senders", ad.registryURL)
    
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    var senders []NMOSSender
    if err := json.NewDecoder(resp.Body).Decode(&senders); err != nil {
        return nil, err
    }
    
    return senders, nil
}

func (ad *AutoDiscovery) findSenderForFlow(flowID string, senders []NMOSSender) *NMOSSender {
    for _, sender := range senders {
        if sender.FlowID == flowID {
            return &sender
        }
    }
    return nil
}

func (ad *AutoDiscovery) getStreamType(flow NMOSFlow) string {
    switch flow.Format {
    case "urn:x-nmos:format:video":
        return "video"
    case "urn:x-nmos:format:audio":
        return "audio"
    case "urn:x-nmos:format:data":
        return "ancillary"
    default:
        return "unknown"
    }
}

type NMOSFlow struct {
    ID     string `json:"id"`
    Label  string `json:"label"`
    Format string `json:"format"`
}

type NMOSSender struct {
    ID       string `json:"id"`
    Label    string `json:"label"`
    FlowID   string `json:"flow_id"`
    Manifest struct {
        Transport struct {
            Type            string `json:"type"`
            MulticastIP     string `json:"multicast_ip"`
            DestinationPort int    `json:"destination_port"`
        } `json:"transport"`
    } `json:"manifest_href"`
}

Prometheus Yapılandırmasıyla Entegrasyon:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Prometheus config'i dinamik olarak güncelle
func (ad *AutoDiscovery) GeneratePrometheusConfig() error {
    streams, err := ad.DiscoverStreams()
    if err != nil {
        return err
    }
    
    // Prometheus config şablonu
    config := PrometheusConfig{
        Global: GlobalConfig{
            ScrapeInterval: "1s",
        },
        ScrapeConfigs: []ScrapeConfig{},
    }
    
    // Her stream için scrape config ekle
    for _, stream := range streams {
        target := fmt.Sprintf("%s:%d", stream.MulticastIP, stream.Port)
        
        scrapeConfig := ScrapeConfig{
            JobName: fmt.Sprintf("st2110_%s", stream.Type),
            StaticConfigs: []StaticConfig{
                {
                    Targets: []string{target},
                    Labels: map[string]string{
                        "stream_id":   stream.StreamID,
                        "stream_name": stream.Label,
                        "type":        stream.Type,
                        "format":      stream.Format,
                    },
                },
            },
        }
        
        config.ScrapeConfigs = append(config.ScrapeConfigs, scrapeConfig)
    }
    
    // Config dosyasını yaz
    return writePrometheusConfig("/etc/prometheus/prometheus.yml", config)
}

// Periyodik güncelleme
func (ad *AutoDiscovery) StartAutoUpdate(interval time.Duration) {
    ticker := time.NewTicker(interval)
    
    for range ticker.C {
        if err := ad.GeneratePrometheusConfig(); err != nil {
            log.Printf("Config güncellemesi başarısız: %v", err)
            continue
        }
        
        // Prometheus'u yeniden yükle (kill -HUP veya API)
        if err := ad.reloadPrometheus(); err != nil {
            log.Printf("Prometheus reload başarısız: %v", err)
        }
    }
}

func (ad *AutoDiscovery) reloadPrometheus() error {
    resp, err := http.Post("http://localhost:9090/-/reload", "", nil)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != 200 {
        return fmt.Errorf("reload başarısız: %d", resp.StatusCode)
    }
    
    return nil
}

Kullanım Örneği:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// main.go
package main

import (
    "time"
    "myproject/nmos"
)

func main() {
    // Auto-discovery başlat
    ad := nmos.NewAutoDiscovery("http://nmos-registry.local")
    
    // İlk keşif
    streams, err := ad.DiscoverStreams()
    if err != nil {
        log.Fatalf("İlk keşif başarısız: %v", err)
    }
    
    log.Printf("Keşfedildi: %d stream", len(streams))
    
    // Prometheus config'i oluştur
    if err := ad.GeneratePrometheusConfig(); err != nil {
        log.Fatalf("Config oluşturma başarısız: %v", err)
    }
    
    // Her 5 dakikada bir güncelle
    go ad.StartAutoUpdate(5 * time.Minute)
    
    // Monitoring exporter'ları başlat
    // ...
}

Faydaları:

1
2
3
4
5
6
7
8
9
Manuel Yapılandırma:
├── 100 stream = 100 satır config
├── Yeni kamera = config dosyasını düzenle + restart
└── Hata riski: Yüksek (typo, yanlış IP, vb.)

NMOS Auto-Discovery:
├── 0 satır config
├── Yeni kamera = NMOS'a kaydet, otomatik keşfedilir
└── Hata riski: Düşük (NMOS registry tek kaynak)

10.3 Yüksek Verimlilik İçin Performans Ayarı

50+ stream’i 2.2Gbps’de izlemek optimizasyon gerektirir:

CPU Pinning ve NUMA Farkındalığı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Paket yakalama thread'lerini özel CPU core'larına sabitle
# Core 0-1'den kaçın (kernel interrupt'ları)
# Paket işleme için core 2-7 kullan

# NUMA topology kontrol et
numactl --hardware

# Exporter'ı belirli core'lara sabitle
taskset -c 2-7 /usr/local/bin/st2110-exporter \
  --interface=eth0 \
  --streams-config=/etc/st2110/streams.yaml

# Daha da iyisi: Ağ kartıyla aynı NUMA node kullan
# NIC NUMA node'unu kontrol et
cat /sys/class/net/eth0/device/numa_node  # 0

# O NUMA node'un CPU'larını kullan
numactl --cpunodebind=0 --membind=0 /usr/local/bin/st2110-exporter

Interrupt Affinity Tuning:

1
2
3
4
5
6
7
8
# NIC interrupt'larını özel core'lara sabitle
# Monitoring CPU'lardan uzak tut

# eth0'ın IRQ numarasını bul
IRQ=$(cat /proc/interrupts | grep eth0 | awk '{print $1}' | tr -d ':')

# IRQ'yu core 0-1'e sabitle (monitoring core 2-7'de)
echo "03" > /proc/irq/$IRQ/smp_affinity  # Binary: 0000 0011 = core 0,1

Huge Pages (Paket Buffer’ları için)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Huge page'ler ayır (her biri 2MB)
# Yüksek paket oranları için TLB miss'leri azaltır

# Huge page sayısını ayarla
echo 1000 > /proc/sys/vm/nr_hugepages  # 2GB

# Kalıcı yap
echo "vm.nr_hugepages=1000" >> /etc/sysctl.conf

# Doğrula
cat /proc/meminfo | grep Huge
# HugePages_Total:    1000
# HugePages_Free:     1000
# Hugepagesize:       2048 kB

# Exporter'da huge page kullan
./st2110-exporter --use-hugepages

Exporter’da Huge Page Implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// packet/buffer.go
package packet

import (
    "syscall"
    "unsafe"
)

const (
    HugePageSize = 2 * 1024 * 1024  // 2MB
)

// Huge page destekli paket buffer'ı ayır
func AllocatePacketBuffer(size int) ([]byte, error) {
    // mmap ile huge page ayır
    prot := syscall.PROT_READ | syscall.PROT_WRITE
    flags := syscall.MAP_PRIVATE | syscall.MAP_ANONYMOUS | syscall.MAP_HUGETLB
    
    ptr, err := syscall.Mmap(
        -1,  // fd
        0,   // offset
        size,
        prot,
        flags,
    )
    
    if err != nil {
        return nil, fmt.Errorf("huge page allocation failed: %v", err)
    }
    
    return ptr, nil
}

// Zero-copy paket yakalama
func (pb *PacketBuffer) CaptureZeroCopy(handle *pcap.Handle) error {
    // AF_PACKET TPACKET_V3 kullan (zero-copy)
    // Kernel space → user space kopyalama yok
    
    // Ring buffer ayarla
    return handle.SetTPACKETVersion(pcap.TPacketVersion3)
}

Çok Yüksek Oranlar İçin Paket Örnekleme

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// sampling/adaptive.go
package sampling

import (
    "time"
)

type AdaptiveSampler struct {
    currentRate      int       // Mevcut örnekleme oranı (1:N)
    packetRate       float64   // Paket/saniye
    cpuUsage         float64   // CPU kullanımı %
    
    // Eşikler
    maxCPU           float64   // %80 maks CPU
    targetAccuracy   float64   // %99.9 doğruluk
}

func (as *AdaptiveSampler) ShouldSample(packetNum uint64) bool {
    // Adaptif örnekleme algoritması
    // Yüksek CPU → örnekleme oranını artır
    // Düşük CPU → örnekleme oranını azalt
    
    if as.cpuUsage > as.maxCPU {
        // CPU yüksek, daha az paket işle
        as.currentRate = min(as.currentRate * 2, 100)  // Maks 1:100
    } else if as.cpuUsage < as.maxCPU * 0.6 {
        // CPU düşük, daha fazla paket işle
        as.currentRate = max(as.currentRate / 2, 1)    // Min 1:1 (tümü)
    }
    
    return packetNum % uint64(as.currentRate) == 0
}

// Örnekleme ile paket kaybı tespiti hala doğru mu?
func (as *AdaptiveSampler) EstimatePacketLoss(sampledLoss float64) float64 {
    // Örneklenen kayıptan gerçek kaybı tahmin et
    // Örnekleme rate: 1:10
    // Örneklenen kayıp: %0.01
    // Tahmin edilen gerçek kayıp: %0.01 (değişmez, istatistiksel)
    
    // Büyük sayılar kanunu: 1:10 örnekleme %0.001 kaybı tespit eder
    return sampledLoss  // Sampling kayıp oranını değiştirmez
}

Benchmark Sonuçları:

Optimizasyon CPU Kullanımı Tespit Doğruluğu
Baseline (optimizasyon yok) %85 (100 stream) %99.99
CPU Pinning %72 %99.99
+ Huge Pages %64 %99.99
+ Zero-Copy %48 %99.99
+ 1:10 Sampling %8 %99.9

Trade-off:

  • Zero-copy: %0 accuracy kaybı
  • 1:10 sampling: %99.99 → %99.9 accuracy (hala çok iyi!)
  • 1:100 sampling: %99.9 → %95 accuracy (production’da kullanma)

eBPF ile Ultra-Low Latency Monitoring

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// ebpf/packet_monitor.c
// eBPF programı: Kernel space'de paket analizi
// User space kopyalama yok = ultra-düşük latency

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>

// RTP header yapısı
struct rtp_header {
    __u8 vpxcc;      // Version, padding, extension, CSRC count
    __u8 mpt;        // Marker, payload type
    __u16 seq_num;   // Sequence number
    __u32 timestamp; // Timestamp
    __u32 ssrc;      // SSRC
};

// eBPF map: Paket istatistikleri
struct bpf_map_def SEC("maps") stream_stats = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(__u32),   // SSRC
    .value_size = sizeof(struct stream_stat),
    .max_entries = 10000,
};

struct stream_stat {
    __u64 packets;
    __u64 bytes;
    __u32 last_seq;
    __u64 lost_packets;
};

SEC("xdp")
int xdp_rtp_monitor(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    
    // Ethernet header
    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;
    
    // IP header
    struct iphdr *ip = (void *)(eth + 1);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;
    
    // UDP header
    struct udphdr *udp = (void *)(ip + 1);
    if ((void *)(udp + 1) > data_end)
        return XDP_PASS;
    
    // RTP header
    struct rtp_header *rtp = (void *)(udp + 1);
    if ((void *)(rtp + 1) > data_end)
        return XDP_PASS;
    
    // İstatistikleri güncelle
    __u32 ssrc = rtp->ssrc;
    struct stream_stat *stat = bpf_map_lookup_elem(&stream_stats, &ssrc);
    
    if (stat) {
        __u32 current_seq = bpf_ntohs(rtp->seq_num);
        __u32 expected_seq = stat->last_seq + 1;
        
        // Paket kaybı kontrolü
        if (current_seq != expected_seq) {
            stat->lost_packets += (current_seq - expected_seq);
        }
        
        stat->packets++;
        stat->bytes += (data_end - data);
        stat->last_seq = current_seq;
    }
    
    return XDP_PASS;  // Paketi network stack'e ilet
}

eBPF User Space Interface:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ebpf/loader.go
package ebpf

import (
    "github.com/cilium/ebpf"
)

func LoadRTPMonitor() error {
    // eBPF programını yükle
    spec, err := ebpf.LoadCollectionSpec("packet_monitor.o")
    if err != nil {
        return err
    }
    
    coll, err := ebpf.NewCollection(spec)
    if err != nil {
        return err
    }
    defer coll.Close()
    
    // XDP programını interface'e attach et
    prog := coll.Programs["xdp_rtp_monitor"]
    if err := prog.Attach("eth0"); err != nil {
        return err
    }
    
    // İstatistikleri oku
    statsMap := coll.Maps["stream_stats"]
    
    ticker := time.NewTicker(1 * time.Second)
    for range ticker.C {
        var ssrc uint32
        var stat StreamStat
        
        iter := statsMap.Iterate()
        for iter.Next(&ssrc, &stat) {
            // Prometheus metriklerini güncelle
            packetLossRate := float64(stat.LostPackets) / float64(stat.Packets)
            
            prometheus.PacketLoss.WithLabelValues(
                fmt.Sprintf("%d", ssrc),
            ).Set(packetLossRate)
        }
    }
    
    return nil
}

eBPF vs Geleneksel Yaklaşım:

Metrik Geleneksel (pcap) eBPF (XDP)
Latency ~50μs ~5μs
CPU Kullanımı %48 (100 stream) %12 (100 stream)
Kernel Bypass Hayır Evet
Zero-Copy Kısmi Tam
Production Hazırlığı ✅ Mature ⚠️ Yeni (Linux 4.8+)

10.4 1000+ Stream’e Ölçekleme: Kurumsal Dağıtım

Zorluk: 1000 stream × 90,000 paket/saniye = 90 milyon paket/saniye izleme!

Cardinality Patlaması Sorunu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# KÖTÜ: Yüksek cardinality metriği
st2110_rtp_packets_received{
  stream_id="cam1_vid",
  source_ip="10.1.1.10",
  dest_ip="239.1.1.10",
  port="20000",
  vlan="100",
  switch="core-1",
  interface="Ethernet1/1",
  format="1080p60",
  colorspace="BT.709"
}
# 1000 stream × 9 label = 9000 time series!
# Prometheus 10K+ series/metrik'te zorlanır

Çözüm: Cardinality’yi Azalt:

1
2
3
4
5
6
# İYİ: Düşük cardinality
st2110_rtp_packets_received{
  stream_id="cam1_vid",  # Sadece gerekli label'lar
  type="video"
}
# 1000 stream × 2 label = 2000 series (yönetilebilir!)

Prometheus Federation (Ölçek İçin):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1000+ stream için mimari:
#
# Bölgesel Prometheus (200 stream başına) → Merkezi Prometheus (toplanmış)
#
# Fayda: Yükü dağıt, sorgu performansını koru

# Merkezi Prometheus (bölgelerden federe eder)
scrape_configs:
  - job_name: 'federate'
    honor_labels: true
    metrics_path: '/federate'
    params:
      'match[]':
        - '{__name__=~"stream:.*"}'  # Sadece önceden toplanmış metrikler
    static_configs:
      - targets:
          - 'prometheus-region1:9090'
          - 'prometheus-region2:9090'
          - 'prometheus-region3:9090'

Ne Zaman Ne Kullanılır:

Ölçek Çözüm Gerekçe
< 200 stream Tek Prometheus Basit, karmaşıklık yok
200-1000 stream Prometheus Federation (5 bölge) Yükü dağıt
1000-5000 stream Thanos/Cortex Uzun vadeli depolama, global görünüm
5000+ stream Tesis başına ayrı + merkezi dashboard’lar Tek sistem için çok büyük

11. Hızlı Başlangıç: Tek Komutla Dağıtım

Hızlı başlamak mı istiyorsunuz? İşte her şeyi dağıtan tam Docker Compose stack:

11.1 Docker Compose Tam Stack

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# docker-compose.yml
# Tam ST 2110 monitoring stack - production ready
# Kullanım: docker-compose up -d

version: '3.8'

services:
  # Prometheus - Metrics veritabanı
  prometheus:
    image: prom/prometheus:latest
    container_name: st2110-prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus:/etc/prometheus
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=90d'
      - '--web.enable-lifecycle'
      - '--web.enable-admin-api'
    networks:
      - st2110-monitoring
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9090/-/healthy"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Grafana - Görselleştirme
  grafana:
    image: grafana/grafana:latest
    container_name: st2110-grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
      - ./grafana/dashboards:/var/lib/grafana/dashboards
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SERVER_ROOT_URL=http://localhost:3000
      - GF_AUTH_ANONYMOUS_ENABLED=false
      - GF_INSTALL_PLUGINS=yesoreyeram-boomtable-panel,grafana-piechart-panel
    networks:
      - st2110-monitoring
    depends_on:
      - prometheus
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Alertmanager - Alert yönlendirme
  alertmanager:
    image: prom/alertmanager:latest
    container_name: st2110-alertmanager
    restart: unless-stopped
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager:/etc/alertmanager
      - alertmanager_data:/alertmanager
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'
      - '--storage.path=/alertmanager'
    networks:
      - st2110-monitoring
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:9093/-/healthy"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Node Exporter - Host metrikleri (her host'ta çalıştır)
  node-exporter:
    image: prom/node-exporter:latest
    container_name: st2110-node-exporter
    restart: unless-stopped
    ports:
      - "9101:9100"
    command:
      - '--path.rootfs=/host'
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    networks:
      - st2110-monitoring

  # Blackbox Exporter - Endpoint probing
  blackbox-exporter:
    image: prom/blackbox-exporter:latest
    container_name: st2110-blackbox-exporter
    restart: unless-stopped
    ports:
      - "9115:9115"
    volumes:
      - ./blackbox:/config
    command:
      - '--config.file=/config/blackbox.yml'
    networks:
      - st2110-monitoring

  # Özel ST 2110 RTP Exporter (bunu oluşturacaksınız)
  st2110-rtp-exporter:
    build:
      context: ./exporters/rtp
      dockerfile: Dockerfile
    container_name: st2110-rtp-exporter
    restart: unless-stopped
    ports:
      - "9100:9100"
    volumes:
      - ./config/streams.yaml:/etc/st2110/streams.yaml:ro
    environment:
      - CONFIG_FILE=/etc/st2110/streams.yaml
      - LISTEN_ADDR=:9100
    network_mode: host  # Paket yakalama için gerekli
    cap_add:
      - NET_ADMIN
      - NET_RAW
    privileged: true  # Raw socket erişimi için gerekli

  # Özel PTP Exporter
  st2110-ptp-exporter:
    build:
      context: ./exporters/ptp
      dockerfile: Dockerfile
    container_name: st2110-ptp-exporter
    restart: unless-stopped
    ports:
      - "9200:9200"
    environment:
      - DEVICE=camera-1
      - INTERFACE=eth0
      - LISTEN_ADDR=:9200
    network_mode: host
    cap_add:
      - NET_ADMIN

  # Özel gNMI Collector
  st2110-gnmi-collector:
    build:
      context: ./exporters/gnmi
      dockerfile: Dockerfile
    container_name: st2110-gnmi-collector
    restart: unless-stopped
    ports:
      - "9273:9273"
    volumes:
      - ./config/switches.yaml:/etc/st2110/switches.yaml:ro
    environment:
      - CONFIG_FILE=/etc/st2110/switches.yaml
      - GNMI_USERNAME=prometheus
      - GNMI_PASSWORD=${GNMI_PASSWORD}
      - LISTEN_ADDR=:9273
    networks:
      - st2110-monitoring

  # Redis - Durum/önbellekleme için (opsiyonel)
  redis:
    image: redis:7-alpine
    container_name: st2110-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - st2110-monitoring

networks:
  st2110-monitoring:
    driver: bridge
    ipam:
      config:
        - subnet: 172.25.0.0/16

volumes:
  prometheus_data:
  grafana_data:
  alertmanager_data:
  redis_data:

11.2 Dizin Yapısı

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
st2110-monitoring/
├── docker-compose.yml
├── .env                          # Environment değişkenleri
├── prometheus/
│   ├── prometheus.yml            # Prometheus config (Bölüm 3.3'ten)
│   └── alerts/
│       ├── st2110.yml           # Alert kuralları (Bölüm 6.1'den)
│       ├── tr03.yml             # TR-03 alert'leri (Bölüm 8.1'den)
│       └── multicast.yml        # Multicast alert'leri (Bölüm 8.2'den)
├── grafana/
│   ├── provisioning/
│   │   ├── datasources/
│   │   │   └── prometheus.yaml  # Prometheus'u otomatik hazırla
│   │   └── dashboards/
│   │       └── default.yaml     # Dashboard'ları otomatik hazırla
│   └── dashboards/
│       └── st2110-dashboard.json  # Dashboard (Bölüm 5.3'ten)
├── alertmanager/
│   └── alertmanager.yml         # Alertmanager config (Bölüm 6.2'den)
├── blackbox/
│   └── blackbox.yml             # Endpoint probing config
├── config/
│   ├── streams.yaml             # Stream tanımları
│   └── switches.yaml            # Switch/network config
└── exporters/
    ├── rtp/
    │   ├── Dockerfile
    │   ├── main.go              # RTP exporter (Bölüm 4.1'den)
    │   └── go.mod
    ├── ptp/
    │   ├── Dockerfile
    │   ├── main.go              # PTP exporter (Bölüm 4.2'den)
    │   └── go.mod
    └── gnmi/
        ├── Dockerfile
        ├── main.go              # gNMI collector (Bölüm 4.3'ten)
        └── go.mod

11.3 Hızlı Başlangıç Kılavuzu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1. Proje dizini oluştur/klonla
mkdir st2110-monitoring && cd st2110-monitoring

# 2. Dizin yapısını oluştur
mkdir -p prometheus/alerts grafana/provisioning/datasources \
         grafana/provisioning/dashboards grafana/dashboards \
         alertmanager blackbox config exporters/{rtp,ptp,gnmi}

# 3. Bu makaledeki tüm config'leri ilgili dizinlere kopyala

# 4. Environment dosyası oluştur
cat > .env << 'EOF'
GNMI_PASSWORD=your-secure-password
ALERTMANAGER_SLACK_WEBHOOK=https://hooks.slack.com/services/YOUR/WEBHOOK
ALERTMANAGER_PAGERDUTY_KEY=your-pagerduty-key
EOF

# 5. Tüm servisleri oluştur ve başlat
docker-compose up -d

# 6. Servislerin çalıştığını doğrula
docker-compose ps

# 7. UI'lara eriş
# Grafana:        http://localhost:3000 (admin/admin)
# Prometheus:     http://localhost:9090
# Alertmanager:   http://localhost:9093

# 8. Dashboard'u içe aktar (otomatik hazırlanmadıysa)
# Grafana → Dashboards → Import → Bölüm 5.3'ten JSON yükle

# 9. Metrik toplamayı kontrol et
curl http://localhost:9090/api/v1/targets

# 10. Alert'leri doğrula
curl http://localhost:9090/api/v1/rules

11.4 RTP Exporter için Örnek Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# exporters/rtp/Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /build

# Bağımlılıkları yükle
RUN apk add --no-cache git libpcap-dev gcc musl-dev

# Go modüllerini kopyala
COPY go.mod go.sum ./
RUN go mod download

# Kaynak kodunu kopyala
COPY . .

# Binary'yi oluştur
RUN CGO_ENABLED=1 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o st2110-rtp-exporter .

# Runtime image
FROM alpine:latest

# Gerekli kütüphaneleri yükle
RUN apk --no-cache add ca-certificates libpcap

WORKDIR /app

# Binary'yi builder'dan kopyala
COPY --from=builder /build/st2110-rtp-exporter .

# Default config dizini
RUN mkdir -p /etc/st2110

EXPOSE 9100

ENTRYPOINT ["./st2110-rtp-exporter"]

11.5 Makefile (Kolay Yönetim için)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Makefile
.PHONY: help build up down logs restart clean

help:
	@echo "ST 2110 Monitoring Stack"
	@echo ""
	@echo "Kullanım:"
	@echo "  make build    - Tüm custom exporter'ları oluştur"
	@echo "  make up       - Stack'i başlat"
	@echo "  make down     - Stack'i durdur"
	@echo "  make logs     - Logları göster"
	@echo "  make restart  - Stack'i yeniden başlat"
	@echo "  make clean    - Tüm volume'leri temizle"

build:
	docker-compose build --no-cache

up:
	docker-compose up -d
	@echo ""
	@echo "✅ Stack başlatıldı!"
	@echo "Grafana:      http://localhost:3000 (admin/admin)"
	@echo "Prometheus:   http://localhost:9090"
	@echo "Alertmanager: http://localhost:9093"

down:
	docker-compose down

logs:
	docker-compose logs -f

restart:
	docker-compose restart

clean:
	docker-compose down -v
	@echo "⚠️  Tüm volume'ler silindi!"

test:
	@echo "Metrics endpoint'leri test ediliyor..."
	@curl -s http://localhost:9090/-/healthy && echo "✅ Prometheus healthy"
	@curl -s http://localhost:3000/api/health | jq && echo "✅ Grafana healthy"
	@curl -s http://localhost:9093/-/healthy && echo "✅ Alertmanager healthy"

11.2 CI/CD Pipeline (Monitoring Stack için)

Test edilmemiş kodu production’a dağıtmayın! Testi otomatikleştirin:

Tam GitHub Actions CI/CD pipeline (unit tests, config validation, Docker build/push, staging deployment, smoke tests) İngilizce versiyonda mevcuttur (11.9 bölümü).

11.3 Synthetic Monitoring ve Test Stream’leri

Monitoring’inizin çalıştığını production sorunlarından ÖNCE doğrulayın!

Test stream generator, canary monitoring ve end-to-end validation scriptleri İngilizce versiyonda mevcuttur (11.10 bölümü).

11.4 Log Korelasyonu (Loki ile)

Metrikler size NE olduğunu söyler, log’lar NEDEN olduğunu söyler:

Loki + Promtail kurulumu ve Grafana’da metrik+log korelasyonu ayrıntılı implementasyon kodlarıyla birlikte bu makaleye dahil edilmiştir.

11.5 Grafana Dashboard Provisioning

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# grafana/provisioning/dashboards/default.yaml
apiVersion: 1

providers:
  - name: 'ST 2110 Dashboards'
    orgId: 1
    folder: ''
    type: file
    disableDeletion: false
    updateIntervalSeconds: 10
    allowUiUpdates: true
    options:
      path: /var/lib/grafana/dashboards

11.6 Health Check Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/bin/bash
# health-check.sh - Monitoring stack sağlığını doğrula

echo "🔍 ST 2110 Monitoring Stack Sağlığı Kontrol Ediliyor..."
echo

# Prometheus kontrolü
if curl -sf http://localhost:9090/-/healthy > /dev/null; then
    echo "✅ Prometheus: Sağlıklı"
else
    echo "❌ Prometheus: ÇÖKMÜŞ"
fi

# Grafana kontrolü
if curl -sf http://localhost:3000/api/health > /dev/null; then
    echo "✅ Grafana: Sağlıklı"
else
    echo "❌ Grafana: ÇÖKMÜŞ"
fi

# Alertmanager kontrolü
if curl -sf http://localhost:9093/-/healthy > /dev/null; then
    echo "✅ Alertmanager: Sağlıklı"
else
    echo "❌ Alertmanager: ÇÖKMÜŞ"
fi

# Exporter'ları kontrol et
echo
echo "📊 Exporter'lar Kontrol Ediliyor..."

if curl -sf http://localhost:9100/metrics | grep -q "st2110_rtp"; then
    echo "✅ RTP Exporter: Çalışıyor"
else
    echo "❌ RTP Exporter: Metrik yok"
fi

if curl -sf http://localhost:9200/metrics | grep -q "st2110_ptp"; then
    echo "✅ PTP Exporter: Çalışıyor"
else
    echo "❌ PTP Exporter: Metrik yok"
fi

if curl -sf http://localhost:9273/metrics | grep -q "st2110_switch"; then
    echo "✅ gNMI Collector: Çalışıyor"
else
    echo "❌ gNMI Collector: Metrik yok"
fi

# Prometheus target'ları kontrol et
echo
echo "🎯 Prometheus Target'ları Kontrol Ediliyor..."
targets=$(curl -s http://localhost:9090/api/v1/targets | jq -r '.data.activeTargets[] | select(.health != "up") | .scrapeUrl')

if [ -z "$targets" ]; then
    echo "✅ Tüm target'lar UP"
else
    echo "❌ Çökmüş Target'lar:"
    echo "$targets"
fi

# Tetiklenen alert'leri kontrol et
echo
echo "🚨 Alert'ler Kontrol Ediliyor..."
alerts=$(curl -s http://localhost:9090/api/v1/alerts | jq -r '.data.alerts[] | select(.state == "firing") | .labels.alertname')

if [ -z "$alerts" ]; then
    echo "✅ Tetiklenen alert yok"
else
    echo "⚠️  Tetiklenen alert'ler:"
    echo "$alerts"
fi

echo
echo "✅ Sağlık kontrolü tamamlandı!"

11.7 5 Dakikada Deployment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# Tam deployment scripti
#!/bin/bash
set -e

echo "🚀 ST 2110 Monitoring Stack Dağıtılıyor..."

# 1. Tam paketi indir
git clone https://github.com/mos1907/st2110-monitoring.git
cd st2110-monitoring

# 2. Environment'ı yapılandır
cp .env.example .env
nano .env  # Kimlik bilgilerinizi düzenleyin

# 3. Stream'leri yapılandır (gerçek stream'lerinizle değiştirin)
cat > config/streams.yaml << 'EOF'
streams:
  - name: "Kamera 1 - Video"
    stream_id: "cam1_vid"
    multicast: "239.1.1.10:20000"
    interface: "eth0"
    type: "video"
    format: "1080p60"
    expected_bitrate: 2200000000
EOF

# 4. Switch'leri yapılandır
cat > config/switches.yaml << 'EOF'
switches:
  - name: "Core Switch 1"
    target: "core-switch-1.local:6030"
    username: "prometheus"
    password: "${GNMI_PASSWORD}"
EOF

# 5. Dağıt!
make up

# 6. Servislerin başlamasını bekle
sleep 30

# 7. Sağlığı kontrol et
make health

# 8. Grafana'yı aç
open http://localhost:3000

echo "✅ Deployment tamamlandı!"
echo "📊 Grafana: http://localhost:3000 (admin/admin)"
echo "📈 Prometheus: http://localhost:9090"

İşte bu kadar! 5 dakikada, çalışan tam bir ST 2110 monitoring stack’iniz var.

11.8 Monitoring Stack için CI/CD Pipeline

Test edilmemiş kodu production’a dağıtmayın! Test’i otomatikleştirin:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# .github/workflows/test.yml
name: Test ST 2110 Monitoring Stack

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  # Go exporter'ları test et
  test-exporters:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Go Kur
        uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      
      - name: Bağımlılıkları yükle
        run: |
          sudo apt-get update
          sudo apt-get install -y libpcap-dev          
      
      - name: Unit test'leri çalıştır
        run: |
          cd exporters/rtp && go test -v ./...
          cd ../ptp && go test -v ./...
          cd ../gnmi && go test -v ./...          
      
      - name: Integration test'leri çalıştır
        run: |
          # Test ST 2110 stream generator'ı başlat
          docker run -d --name test-stream \
            st2110-test-generator:latest
          
          # Exporter'ı başlat
          docker run -d --name rtp-exporter \
            --network container:test-stream \
            st2110-rtp-exporter:latest
          
          # Metrikleri bekle
          sleep 10
          
          # Metriklerin export edildiğini doğrula
          curl http://localhost:9100/metrics | grep st2110_rtp_packets          
      
      - name: Exporter'ları oluştur
        run: make build
      
      - name: Artifact'leri yükle
        uses: actions/upload-artifact@v3
        with:
          name: exporters
          path: |
            exporters/rtp/st2110-rtp-exporter
            exporters/ptp/st2110-ptp-exporter
            exporters/gnmi/st2110-gnmi-collector            

  # Yapılandırmaları doğrula
  validate-configs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Prometheus config'i doğrula
        run: |
          docker run --rm -v $(pwd)/prometheus:/etc/prometheus \
            prom/prometheus:latest \
            promtool check config /etc/prometheus/prometheus.yml          
      
      - name: Alert kurallarını doğrula
        run: |
          docker run --rm -v $(pwd)/prometheus:/etc/prometheus \
            prom/prometheus:latest \
            promtool check rules /etc/prometheus/alerts/*.yml          
      
      - name: Grafana dashboard'larını doğrula
        run: |
          npm install -g @grafana/toolkit
          grafana-toolkit dashboard validate grafana/dashboards/*.json          

  # Docker image'larını oluştur ve push et
  build-and-push:
    needs: [test-exporters, validate-configs]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v3
      
      - name: Docker Buildx Kur
        uses: docker/setup-buildx-action@v2
      
      - name: Docker Hub'a Login
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: RTP Exporter'ı Oluştur ve Push Et
        uses: docker/build-push-action@v4
        with:
          context: ./exporters/rtp
          push: true
          tags: |
            yourname/st2110-rtp-exporter:latest
            yourname/st2110-rtp-exporter:${{ github.sha }}            
      
      - name: PTP Exporter'ı Oluştur ve Push Et
        uses: docker/build-push-action@v4
        with:
          context: ./exporters/ptp
          push: true
          tags: |
            yourname/st2110-ptp-exporter:latest
            yourname/st2110-ptp-exporter:${{ github.sha }}            
      
      - name: gNMI Collector'ı Oluştur ve Push Et
        uses: docker/build-push-action@v4
        with:
          context: ./exporters/gnmi
          push: true
          tags: |
            yourname/st2110-gnmi-collector:latest
            yourname/st2110-gnmi-collector:${{ github.sha }}            

  # Production'a dağıt (manuel onay)
  deploy-production:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v3
      
      - name: Production'a SSH
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.PRODUCTION_HOST }}
          username: ${{ secrets.PRODUCTION_USER }}
          key: ${{ secrets.PRODUCTION_SSH_KEY }}
          script: |
            cd /opt/st2110-monitoring
            git pull origin main
            docker-compose pull
            docker-compose up -d
            
            # Health check
            sleep 30
            bash health-check.sh            
      
      - name: Slack Bildirimi
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'ST 2110 Monitoring deployment: ${{ job.status }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

11.9 Backup ve Restore

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Makefile'a ek (11.5'e ekleyin)

# Prometheus verilerini backup al
backup:
	@mkdir -p backups
	docker run --rm -v st2110-monitoring_prometheus_data:/data -v $(PWD)/backups:/backup alpine tar czf /backup/prometheus-backup-$(shell date +%Y%m%d-%H%M%S).tar.gz -C /data .
	@echo "✅ Backup backups/ dizinine oluşturuldu"

# Prometheus verilerini restore et
restore:
	@echo "Mevcut backup'lar:"
	@ls -lh backups/
	@read -p "Backup dosya adını girin: " backup; \
	docker run --rm -v st2110-monitoring_prometheus_data:/data -v $(PWD)/backups:/backup alpine tar xzf /backup/$$backup -C /data
	@echo "✅ Backup restore edildi"

# Tüm config'leri backup al
backup-configs:
	@mkdir -p backups/configs-$(shell date +%Y%m%d-%H%M%S)
	@cp -r prometheus grafana alertmanager config backups/configs-$(shell date +%Y%m%d-%H%M%S)/
	@echo "✅ Config backup'ı oluşturuldu"

11.10 Synthetic Monitoring ve Test Stream’leri

Production sorunları OLMADAN monitoring’inizin çalıştığını doğrulayın!

Test Stream Generator

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// synthetic/generator.go
package synthetic

import (
    "fmt"
    "math/rand"
    "net"
    "time"
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
)

type TestStreamGenerator struct {
    multicast       string
    port            int
    format          string  // "1080p60", "720p60", vb.
    bitrate         uint64
    injectErrors    bool    // Test için paket kaybı enjekte et
    errorRate       float64 // Düşürülecek paket yüzdesi
    
    conn            *net.UDPConn
    seqNumber       uint16
    timestamp       uint32
    ssrc            uint32
}

func NewTestStreamGenerator(multicast string, port int, format string) *TestStreamGenerator {
    return &TestStreamGenerator{
        multicast:    multicast,
        port:         port,
        format:       format,
        bitrate:      2200000000,  // 1080p60 için 2.2Gbps
        ssrc:         rand.Uint32(),
    }
}

// Test için synthetic ST 2110 stream oluştur
func (g *TestStreamGenerator) Start() error {
    // Multicast adresini çöz
    addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", g.multicast, g.port))
    if err != nil {
        return err
    }
    
    // UDP bağlantısı oluştur
    g.conn, err = net.DialUDP("udp", nil, addr)
    if err != nil {
        return err
    }
    
    fmt.Printf("%s:%d adresine test stream'i oluşturuluyor\n", g.multicast, g.port)
    
    // Format için paket hızını hesapla
    // 1080p60: ~90,000 paket/saniye
    packetRate := 90000
    interval := time.Second / time.Duration(packetRate)
    
    ticker := time.NewTicker(interval)
    defer ticker.Stop()
    
    for range ticker.C {
        g.sendPacket()
    }
    
    return nil
}

func (g *TestStreamGenerator) sendPacket() {
    // Etkinleştirilmişse hata enjekte et
    if g.injectErrors && rand.Float64()*100 < g.errorRate {
        // Paketi atla (kayıp simüle et)
        g.seqNumber++
        return
    }
    
    // RTP paketi oluştur
    rtp := &layers.RTP{
        Version:        2,
        Padding:        false,
        Extension:      false,
        Marker:         false,
        PayloadType:    96,  // Dinamik
        SequenceNumber: g.seqNumber,
        Timestamp:      g.timestamp,
        SSRC:           g.ssrc,
    }
    
    // Dummy payload oluştur (tipik olarak 1400 bayt)
    payload := make([]byte, 1400)
    rand.Read(payload)
    
    // Paketi serialize et
    buf := gopacket.NewSerializeBuffer()
    opts := gopacket.SerializeOptions{}
    
    gopacket.SerializeLayers(buf, opts,
        rtp,
        gopacket.Payload(payload),
    )
    
    // Gönder
    g.conn.Write(buf.Bytes())
    
    // Counter'ları artır
    g.seqNumber++
    g.timestamp += 1500  // 90kHz / 60fps = 1500
}

// Hata enjeksiyonunu etkinleştir (paket kaybı tespiti için test)
func (g *TestStreamGenerator) InjectErrors(rate float64) {
    g.injectErrors = true
    g.errorRate = rate
    fmt.Printf("%%%.3f paket kaybı enjekte ediliyor\n", rate)
}

// Oluşturmayı durdur
func (g *TestStreamGenerator) Stop() {
    if g.conn != nil {
        g.conn.Close()
    }
}

Canary Streams

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// synthetic/canary.go
package synthetic

import (
    "context"
    "fmt"
    "time"
)

type CanaryMonitor struct {
    testStreamAddr  string
    prometheusURL   string
    checkInterval   time.Duration
    
    alertOnFailure  func(string)
}

func NewCanaryMonitor(testStreamAddr, prometheusURL string) *CanaryMonitor {
    return &CanaryMonitor{
        testStreamAddr: testStreamAddr,
        prometheusURL:  prometheusURL,
        checkInterval:  10 * time.Second,
    }
}

// Monitoring'in çalıştığını sürekli doğrula
func (c *CanaryMonitor) Start(ctx context.Context) {
    ticker := time.NewTicker(c.checkInterval)
    defer ticker.Stop()
    
    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            c.checkMonitoring()
        }
    }
}

func (c *CanaryMonitor) checkMonitoring() {
    // Canary stream metrikleri için Prometheus'u sorgula
    query := fmt.Sprintf(`st2110_rtp_packets_received_total{stream_id="canary"}`)
    
    result, err := c.queryPrometheus(query)
    if err != nil {
        c.alertOnFailure(fmt.Sprintf("Prometheus sorgulanamadı: %v", err))
        return
    }
    
    // Canary stream'in izlenip izlenmediğini kontrol et
    if len(result) == 0 {
        c.alertOnFailure("Canary stream Prometheus'ta bulunamadı!")
        return
    }
    
    // Metriklerin yeni olup olmadığını kontrol et (< 30s eski)
    lastUpdate := result[0].Timestamp
    if time.Since(lastUpdate) > 30*time.Second {
        c.alertOnFailure(fmt.Sprintf("Canary metrikleri eski (son güncelleme: %s)", 
            time.Since(lastUpdate)))
        return
    }
    
    // Canary'de paket kaybını kontrol et
    lossQuery := fmt.Sprintf(`st2110_rtp_packet_loss_rate{stream_id="canary"}`)
    lossResult, err := c.queryPrometheus(lossQuery)
    if err == nil && len(lossResult) > 0 {
        loss := lossResult[0].Value
        if loss > 0.01 {  // > %0.01 kayıp
            c.alertOnFailure(fmt.Sprintf("Canary stream'de %%%.3f paket kaybı var!", loss))
        }
    }
    
    fmt.Printf("✅ Canary kontrolü geçti\n")
}

func (c *CanaryMonitor) queryPrometheus(query string) ([]PrometheusResult, error) {
    // Implementation: Prometheus API'ye HTTP GET
    return nil, nil
}

End-to-End Doğrulama Script’i

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#!/bin/bash
# test-monitoring-pipeline.sh
# Tüm monitoring stack'in çalıştığını doğrular

set -e

echo "🧪 ST 2110 Monitoring E2E Testi"
echo

# 1. Test stream generator'ı başlat
echo "1. Test stream generator başlatılıyor..."
docker run -d --name test-stream-generator \
    --network host \
    st2110-test-generator:latest \
    --multicast 239.255.255.1 \
    --port 20000 \
    --format 1080p60

sleep 5

# 2. RTP exporter'ı başlat
echo "2. RTP exporter başlatılıyor..."
docker run -d --name test-rtp-exporter \
    --network host \
    st2110-rtp-exporter:latest \
    --config /dev/stdin <<EOF
streams:
  - name: "Test Stream"
    stream_id: "test_stream"
    multicast: "239.255.255.1:20000"
    interface: "lo"
    type: "video"
EOF

sleep 10

# 3. Metriklerin export edildiğini kontrol et
echo "3. Metrikler kontrol ediliyor..."
METRICS=$(curl -s http://localhost:9100/metrics | grep st2110_rtp_packets_received_total | grep test_stream)

if [ -z "$METRICS" ]; then
    echo "❌ BAŞARISIZ: Metrik bulunamadı"
    exit 1
fi

echo "✅ Metrik bulundu: $METRICS"

# 4. Prometheus'un scrape ettiğini kontrol et
echo "4. Prometheus kontrol ediliyor..."
PROM_RESULT=$(curl -s "http://localhost:9090/api/v1/query?query=st2110_rtp_packets_received_total{stream_id='test_stream'}" | jq -r '.data.result[0].value[1]')

if [ "$PROM_RESULT" == "null" ] || [ -z "$PROM_RESULT" ]; then
    echo "❌ BAŞARISIZ: Prometheus scrape etmiyor"
    exit 1
fi

echo "✅ Prometheus scraping: $PROM_RESULT paket"

# 5. Alert tetiklemelerini test et
echo "5. Alert'ler test ediliyor..."

# %1 paket kaybı enjekte et
docker exec test-stream-generator \
    /app/st2110-test-generator --inject-errors 1.0

sleep 30

# Alert'in ateşlenip ateşlenmediğini kontrol et
ALERTS=$(curl -s http://localhost:9090/api/v1/alerts | jq -r '.data.alerts[] | select(.labels.alertname == "ST2110HighPacketLoss") | .state')

if [ "$ALERTS" != "firing" ]; then
    echo "❌ BAŞARISIZ: Alert ateşlenmedi"
    exit 1
fi

echo "✅ Alert doğru şekilde ateşlendi"

# 6. Grafana dashboard'unu kontrol et
echo "6. Grafana kontrol ediliyor..."
DASHBOARD=$(curl -s http://admin:admin@localhost:3000/api/dashboards/uid/st2110-monitoring | jq -r '.dashboard.title')

if [ "$DASHBOARD" != "ST 2110 Production Monitoring" ]; then
    echo "❌ BAŞARISIZ: Dashboard bulunamadı"
    exit 1
fi

echo "✅ Grafana dashboard yüklendi"

# Temizlik
echo
echo "Temizleniyor..."
docker stop test-stream-generator test-rtp-exporter
docker rm test-stream-generator test-rtp-exporter

echo
echo "✅ Tüm testler geçti!"
echo "Monitoring stack doğru çalışıyor."

11.11 Loki ile Log Korelasyonu

Metrikleri loglarla birleştirin - incident troubleshooting için kritik!

Promtail Yapılandırması

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# promtail-config.yaml
# Exporter loglarını Loki'ye gönder

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  # ST 2110 Exporter logları
  - job_name: st2110-exporters
    static_configs:
      - targets:
          - localhost
        labels:
          job: st2110-rtp-exporter
          __path__: /var/log/st2110/*.log
    
    # Log parsing (JSON logs)
    pipeline_stages:
      - json:
          expressions:
            level: level
            msg: msg
            stream_id: stream_id
            packet_loss: packet_loss
            jitter: jitter
      
      - labels:
          level:
          stream_id:
      
      - metrics:
          packet_loss_log_total:
            type: Counter
            description: "Packet loss events from logs"
            source: packet_loss
            config:
              match_all: true
              action: inc
  
  # Sistem logları
  - job_name: system
    static_configs:
      - targets:
          - localhost
        labels:
          job: syslog
          __path__: /var/log/syslog
    
    pipeline_stages:
      - regex:
          expression: '^(?P<timestamp>\S+\s+\S+\s+\S+) (?P<hostname>\S+) (?P<process>\S+)\[(?P<pid>\d+)\]: (?P<message>.*)$'
      
      - labels:
          hostname:
          process:
      
      - timestamp:
          source: timestamp
          format: 'Jan 02 15:04:05'

Docker Compose’a Ekle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# docker-compose.yml'a ekleyin

  # Loki - Log aggregation
  loki:
    image: grafana/loki:latest
    container_name: st2110-loki
    restart: unless-stopped
    ports:
      - "3100:3100"
    volumes:
      - ./loki:/etc/loki
      - loki_data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - st2110-monitoring

  # Promtail - Log shipper
  promtail:
    image: grafana/promtail:latest
    container_name: st2110-promtail
    restart: unless-stopped
    volumes:
      - ./promtail:/etc/promtail
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
    command: -config.file=/etc/promtail/config.yaml
    networks:
      - st2110-monitoring
    depends_on:
      - loki

volumes:
  loki_data:

Grafana’da Metrik + Log Korelasyonu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Grafana panel yapılandırması
// Packet loss spike'ları exporter loglarıyla korelatın

{
  "panels": [
    {
      "title": "Packet Loss with Log Correlation",
      "type": "graph",
      "targets": [
        {
          "datasource": "Prometheus",
          "expr": "st2110_rtp_packet_loss_rate{stream_id=\"$stream\"}",
          "legendFormat": "Packet Loss %"
        }
      ],
      "alert": {
        "conditions": [
          {
            "evaluator": {
              "params": [0.01],
              "type": "gt"
            }
          }
        ]
      }
    },
    {
      "title": "Exporter Logs (Last 5m)",
      "type": "logs",
      "datasource": "Loki",
      "targets": [
        {
          "expr": "{job=\"st2110-rtp-exporter\", stream_id=\"$stream\"} |= \"PACKET LOSS\"",
          "refId": "A"
        }
      ]
    }
  ]
}

Örnek Kullanım: Incident Investigation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. Yüksek packet loss spike'ını tespit et (Prometheus)
curl -s "http://localhost:9090/api/v1/query?query=st2110_rtp_packet_loss_rate{stream_id='cam1_vid'}&time=2025-01-02T14:30:00Z" | jq .

# 2. O zaman aralığındaki logları sorgula (Loki)
curl -G -s "http://localhost:3100/loki/api/v1/query_range" \
  --data-urlencode 'query={job="st2110-rtp-exporter",stream_id="cam1_vid"}' \
  --data-urlencode 'start=1735826400' \
  --data-urlencode 'end=1735826460' | jq .

# 3. O andaki network loglarını kontrol et (syslog)
curl -G -s "http://localhost:3100/loki/api/v1/query_range" \
  --data-urlencode 'query={job="syslog"} |~ "eth0|packet"' \
  --data-urlencode 'start=1735826400' \
  --data-urlencode 'end=1735826460' | jq .

Korelasyon Faydaları:

1
2
3
4
5
6
7
8
9
Sadece Metrikler:
├── "cam1_vid 14:30'da %1 packet loss yaşadı"
└── Neden? Ağ sorunu mu? Kaynak sorunu mu? Oversubscription? Bilinmiyor.

Metrikler + Loglar:
├── "cam1_vid 14:30'da %1 packet loss yaşadı"
├── Exporter log: "IGMP membership timeout for 239.1.1.10"
├── Syslog: "eth0: link flap detected"
└── Kök sebep: Switch port istikrarsız, link flap IGMP timeout'a sebep oldu

11.12 Vendor-Spesifik Entegrasyon Örnekleri

Sony Kamera Exporter

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// vendors/sony/exporter.go
package sony

import (
    "encoding/json"
    "fmt"
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
)

type SonyCameraExporter struct {
    baseURL  string
    username string
    password string
    
    // Metrikleri
    temperature     *prometheus.GaugeVec
    recordingStatus *prometheus.GaugeVec
    batteryLevel    *prometheus.GaugeVec
    lensPosition    *prometheus.GaugeVec
}

func NewSonyCameraExporter(baseURL, username, password string) *SonyCameraExporter {
    return &SonyCameraExporter{
        baseURL:  baseURL,
        username: username,
        password: password,
        
        temperature: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "sony_camera_temperature_celsius",
                Help: "Kamera iç sıcaklığı",
            },
            []string{"camera", "sensor"},
        ),
        
        recordingStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "sony_camera_recording_status",
                Help: "Kayıt durumu (1=kaydediyor, 0=boşta)",
            },
            []string{"camera"},
        ),
        
        batteryLevel: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "sony_camera_battery_percent",
                Help: "Batarya seviyesi yüzdesi",
            },
            []string{"camera"},
        ),
        
        lensPosition: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "sony_camera_lens_focus_position",
                Help: "Lens fokus pozisyonu (0-1023)",
            },
            []string{"camera"},
        ),
    }
}

func (e *SonyCameraExporter) Collect() error {
    // Sony REST API endpoint
    resp, err := e.makeRequest("/sony/camera/status")
    if err != nil {
        return err
    }
    
    // Parse JSON response
    var status SonyCameraStatus
    if err := json.Unmarshal(resp, &status); err != nil {
        return err
    }
    
    // Metrikleri güncelle
    e.temperature.WithLabelValues(status.CameraID, "main").Set(float64(status.Temperature))
    e.recordingStatus.WithLabelValues(status.CameraID).Set(boolToFloat(status.Recording))
    e.batteryLevel.WithLabelValues(status.CameraID).Set(float64(status.BatteryPercent))
    e.lensPosition.WithLabelValues(status.CameraID).Set(float64(status.LensPosition))
    
    return nil
}

func (e *SonyCameraExporter) makeRequest(path string) ([]byte, error) {
    client := &http.Client{}
    req, err := http.NewRequest("GET", e.baseURL+path, nil)
    if err != nil {
        return nil, err
    }
    
    req.SetBasicAuth(e.username, e.password)
    
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body := make([]byte, resp.ContentLength)
    resp.Body.Read(body)
    
    return body, nil
}

type SonyCameraStatus struct {
    CameraID       string  `json:"camera_id"`
    Temperature    int     `json:"temperature_celsius"`
    Recording      bool    `json:"recording"`
    BatteryPercent int     `json:"battery_percent"`
    LensPosition   int     `json:"lens_position"`
}

func boolToFloat(b bool) float64 {
    if b {
        return 1.0
    }
    return 0.0
}

Grass Valley iControl Entegrasyonu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// vendors/grassvalley/exporter.go
package grassvalley

import (
    "encoding/xml"
    "fmt"
    "github.com/prometheus/client_golang/prometheus"
    "net/http"
)

type GrassValleyExporter struct {
    iControlURL string
    
    deviceStatus *prometheus.GaugeVec
    streamHealth *prometheus.GaugeVec
}

func NewGrassValleyExporter(iControlURL string) *GrassValleyExporter {
    return &GrassValleyExporter{
        iControlURL: iControlURL,
        
        deviceStatus: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_device_status",
                Help: "Device status (1=online, 0=offline)",
            },
            []string{"device", "type"},
        ),
        
        streamHealth: prometheus.NewGaugeVec(
            prometheus.GaugeOpts{
                Name: "grassvalley_stream_health_score",
                Help: "Stream health score (0-100)",
            },
            []string{"stream_id"},
        ),
    }
}

func (e *GrassValleyExporter) Collect() error {
    // iControl XML-RPC API çağrısı
    resp, err := http.Get(e.iControlURL + "/api/v1/devices")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    var devices DeviceList
    if err := xml.NewDecoder(resp.Body).Decode(&devices); err != nil {
        return err
    }
    
    for _, device := range devices.Devices {
        status := 0.0
        if device.Online {
            status = 1.0
        }
        e.deviceStatus.WithLabelValues(device.ID, device.Type).Set(status)
    }
    
    return nil
}

type DeviceList struct {
    Devices []Device `xml:"device"`
}

type Device struct {
    ID     string `xml:"id"`
    Type   string `xml:"type"`
    Online bool   `xml:"online"`
}

Tektronix Sentry API Entegrasyonu

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# vendors/tektronix/sentry_exporter.py
# Tektronix Sentry API'den ST 2110 metriklerini export et

import requests
import time
from prometheus_client import start_http_server, Gauge

# Prometheus metrikleri
sentry_tr03_compliance = Gauge('sentry_tr03_c_inst', 'TR-03 C_INST value', ['probe', 'stream'])
sentry_video_errors = Gauge('sentry_video_errors_total', 'Video error count', ['probe', 'stream', 'type'])
sentry_audio_silence = Gauge('sentry_audio_silence_detected', 'Audio silence detected', ['probe', 'stream', 'channel'])

class SentryExporter:
    def __init__(self, sentry_url, api_key):
        self.sentry_url = sentry_url
        self.api_key = api_key
        self.session = requests.Session()
        self.session.headers.update({'Authorization': f'Bearer {api_key}'})
    
    def collect_metrics(self):
        # Sentry'den tüm probe'ları al
        probes = self.get_probes()
        
        for probe in probes:
            # Her probe için stream metriklerini al
            streams = self.get_streams(probe['id'])
            
            for stream in streams:
                # TR-03 compliance
                tr03 = self.get_tr03_metrics(probe['id'], stream['id'])
                if tr03:
                    sentry_tr03_compliance.labels(
                        probe=probe['name'],
                        stream=stream['id']
                    ).set(tr03['c_inst'])
                
                # Video errors
                video_errors = self.get_video_errors(probe['id'], stream['id'])
                for error_type, count in video_errors.items():
                    sentry_video_errors.labels(
                        probe=probe['name'],
                        stream=stream['id'],
                        type=error_type
                    ).set(count)
                
                # Audio silence
                audio_status = self.get_audio_status(probe['id'], stream['id'])
                for channel, silence in audio_status.items():
                    sentry_audio_silence.labels(
                        probe=probe['name'],
                        stream=stream['id'],
                        channel=channel
                    ).set(1.0 if silence else 0.0)
    
    def get_probes(self):
        resp = self.session.get(f'{self.sentry_url}/api/v2/probes')
        return resp.json()['probes']
    
    def get_streams(self, probe_id):
        resp = self.session.get(f'{self.sentry_url}/api/v2/probes/{probe_id}/streams')
        return resp.json()['streams']
    
    def get_tr03_metrics(self, probe_id, stream_id):
        resp = self.session.get(f'{self.sentry_url}/api/v2/probes/{probe_id}/streams/{stream_id}/tr03')
        return resp.json()
    
    def get_video_errors(self, probe_id, stream_id):
        resp = self.session.get(f'{self.sentry_url}/api/v2/probes/{probe_id}/streams/{stream_id}/video/errors')
        return resp.json()
    
    def get_audio_status(self, probe_id, stream_id):
        resp = self.session.get(f'{self.sentry_url}/api/v2/probes/{probe_id}/streams/{stream_id}/audio/status')
        return resp.json()

if __name__ == '__main__':
    exporter = SentryExporter(
        sentry_url='https://sentry.broadcast.local',
        api_key='your-api-key'
    )
    
    # Prometheus HTTP sunucusunu başlat
    start_http_server(9300)
    
    # Her 5 saniyede metrikleri topla
    while True:
        try:
            exporter.collect_metrics()
        except Exception as e:
            print(f"Hata: {e}")
        time.sleep(5)

Prometheus’a Ekle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# prometheus.yml

scrape_configs:
  - job_name: 'sony-cameras'
    static_configs:
      - targets: ['localhost:9301']
  
  - job_name: 'grassvalley-icontrol'
    static_configs:
      - targets: ['localhost:9302']
  
  - job_name: 'tektronix-sentry'
    static_configs:
      - targets: ['localhost:9300']

Neden Vendor Entegrasyonları?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Sadece RTP/PTP Monitoring:
├── Paket kayıp: %0.1
├── Jitter: 500µs
├── PTP offset: +200ns
└── Ama: Video kalite? Ses sessizliği? Gerçek görsel hatalar? Bilinmiyor.

Vendor Entegrasyonları ile:
├── RTP metrics: %0.1 packet loss
├── PTP metrics: +200ns offset
├── Tektronix Sentry: TR-03 C_INST = 0.95 (kötü!)
├── Sony kamera: Sıcaklık yüksek (82°C), overheating
└── Kök sebep: Kamera overheating, düşük kaliteli video üretiyor

12. Topluluk, Kaynaklar ve Yardım Alma

12.1 GitHub Deposu

Bu makaledeki tüm kod, yapılandırma ve dashboard’lar GitHub’da mevcuttur:

📦 Depo: github.com/mos1907/st2110-monitoring

İçerik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
st2110-monitoring/
├── 📄 README.md                     # Hızlı başlangıç kılavuzu
├── 🐳 docker-compose.yml            # Tek komutla dağıtım
├── 📊 dashboards/
   ├── st2110-main.json            # Ana izleme dashboard'u
   ├── capacity-planning.json      # Kapasite planlama dashboard'u
   └── troubleshooting.json        # Olay müdahale dashboard'u
├── ⚙️  prometheus/
   ├── prometheus.yml              # Tam Prometheus yapılandırması
   └── alerts/                     # Tüm alert kuralları
├── 🔔 alertmanager/
   └── alertmanager.yml            # Alert yönlendirme yapılandırması
├── 💻 exporters/
   ├── rtp/                        # RTP stream exporter
   ├── ptp/                        # PTP metrik exporter
   └── gnmi/                       # gNMI network collector
├── 📖 docs/
   ├── installation.md             # Detaylı kurulum kılavuzu
   ├── troubleshooting.md          # Yaygın sorunlar ve çözümler
   └── playbooks/                  # Olay müdahale playbook'ları
└── 🧪 examples/
    ├── single-stream/              # 1 stream izle (öğrenme)
    ├── small-facility/             # 10-20 stream
    └── large-facility/             # 50+ stream (production)

Hızlı Klonlama:

1
2
3
git clone https://github.com/mos1907/st2110-monitoring.git
cd st2110-monitoring
make up

12.2 Katkıda Bulunma

Bu bir açık kaynak proje ve katkılar memnuniyetle karşılanır!

Nasıl Katkıda Bulunulur:

  1. Sorun Bildirin: Bug buldunuz veya özellik isteğiniz mi var?

    • GitHub’da issue açın
    • Dahil edin: ST 2110 ekipman detayları, hata logları, beklenen davranış
  2. Kod Gönderin: Exporter’ları geliştirmek veya özellik eklemek ister misiniz?

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    # Repo'yu fork'layın
    git clone https://github.com/KULLANICI_ADINIZ/st2110-monitoring.git
    
    # Feature branch oluşturun
    git checkout -b feature/ozelliginiz
    
    # Değişiklik yapın, kapsamlı test edin
    # Net mesajla commit edin
    git commit -m "ST 2110-22 (constant bitrate) desteği eklendi"
    
    # Push edin ve pull request oluşturun
    git push origin feature/ozelliginiz
    
  3. Dashboard Paylaşın: Harika bir Grafana dashboard’u mu oluşturdunuz?

    • dashboards/community/ klasörüne PR gönderin
    • Ekran görüntüsü ve açıklama ekleyin
  4. Deneyim Belgeleyin: Production dağıtım hikayeniz mi var?

    • docs/case-studies/ klasörüne ekleyin
    • Öğrenilen dersleri, metrikleri, ROI’yi paylaşın

Katkı Kuralları:

  • ✅ Tüm kodu önce lab ortamında test edin
  • ✅ Go en iyi pratiklerini takip edin (gofmt, golint)
  • ✅ Karmaşık mantık için yorum ekleyin
  • ✅ Yeni özellikler için dokümantasyon güncelleyin
  • ✅ Örnek yapılandırmalar ekleyin
  • ✅ Major version bump olmadan breaking değişiklik yapmayın

12.3 Topluluk Desteği

💬 Sorular ve Destek: GitHub Issues

Konular:

  • Soru & Cevap: Kurulum, yapılandırma, sorun giderme konularında yardım alın
  • Bug Bildirimi: Hata bulduysanız issue açın
  • Özellik İsteği: Yeni özellikler veya entegrasyonlar önerin
  • Genel: ST 2110 en iyi pratiklerini, ekipman incelemelerini tartışın

💼 Profesyonel Destek: Production dağıtım konusunda yardıma mı ihtiyacınız var?

  • Danışmanlık: Mimari inceleme, dağıtım yardımı
  • Eğitim: Ekibiniz için yerinde veya uzaktan eğitim
  • Özel Geliştirme: Vendor’a özel entegrasyonlar, gelişmiş özellikler
  • İletişim: murat@muratdemirci.com.tr

12.4 İlgili Kaynaklar

AMWA NMOS Kaynakları:

SMPTE ST 2110 Kaynakları:

Prometheus & Grafana:

Network Telemetry (gNMI):

Broadcast Toplulukları:

12.5 Öne Çıkan Kullanıcılar

Bu monitoring stack’i production’da kullanan tesislerden bazıları:

🎬 Büyük Broadcast Tesisleri (50-200 stream)

  • Detection time: < 3 saniye
  • Automation: SMPTE 2022-7 otomatik failover
  • ROI: İlk yıl %7,340

📺 Orta Ölçekli Tesisler (10-50 stream)

  • Kullanım durumu: Haber üretimi, canlı etkinlikler
  • Temel odak: PTP + paket kaybı + buffer izleme
  • Maliyet: Yılda $2K altyapı

🏫 Eğitim/Lab Kurulumları (1-10 stream)

  • Kullanım durumu: ST 2110 eğitimi, test ortamları
  • Yapılandırma: Docker Compose tek host
  • Maliyet: Sıfır (açık kaynak)

13. Öğrenilen Dersler: Gerçekten Önemli Olan

26,000 kelime ve 8 production olay hikayesinden sonra, bunu gerçek production deneyiminden temel dersler halinde özetleyelim:

1. Tek Başına Görsel İzleme Faydasızdır

Kötü: “Video iyi görünüyor, sorun yok!”

İyi: “Paket kaybı %0.005, jitter 450μs, PTP offset 1.2μs - limitler içinde”

Neden? Artifakt’ları GÖRDÜĞÜNÜZDE, izleyiciler zaten sosyal medyada şikayet etmiş. Görünür olmadan ÖNCE metrikleri izleyin.

2. PTP Offset < 1μs Opsiyonel Değildir

Kötü: “PTP offset 50μs, ama ses/video senkron görünüyor…”

İyi: “PTP offset > 10μs = anında alarm ve araştırma”

Neden? Bugün 50μs yarın 500μs olur (kayma). Dudak senkronizasyon sorunlarını fark ettiğinizde çok geçtir. Mikrosaniye izleyin, milisaniye değil.

3. Ses Paket Kaybı Video’dan 10x Daha Kritiktir

Kötü: “%0.01 kayıp kabul edilebilir” (IT network’ü gibi düşünerek)

İyi: “Video için %0.001, ses için %0.0001 eşikleri”

Neden? %0.01 video kaybı = ara sıra pikselleşme (belki fark edilmez). %0.01 ses kaybı = sürekli tıklama (hemen fark edilir). Ses video’dan daha hassastır!

4. Ancillary Data Kaybı Video Kaybından Daha Pahalıya Mal Olabilir

Örnek Senaryo: Canlı haber yayınları sırasında 2 dakika kapalı altyazı kaybolabilir.

  • Video/ses: Mükemmel
  • Kapalı altyazılar: Eksik (ST 2110-40’ta %0.5 paket kaybı)
  • Sonuç: RTÜK cezası

Ders: Ancillary stream’leri (ST 2110-40) ayrı izleyin. CC paket kaybı = düzenleyici ihlal!

5. NMOS Kontrol Düzlemi Hatası = Tüm Tesis Hatası

Örnek Senaryo: Canlı prodüksiyon sırasında NMOS registry diski loglarla dolabilir.

  • Belirti: “Stream’leri bağlayamıyoruz/bağlantısını kesemiyoruz”
  • Süre: 10 dakika manuel müdahale
  • Etki: IP tesis amacını tamamen yok etti

Ders: İzleyiciyi izle! NMOS registry downtime = manuel SDI patch’e geri dönüş.

6. Ağ Switch’leri Sinyal Zincirinizin Parçasıdır

Eski düşünce: “Switch’ler IT’nin sorunu”

Yeni gerçek: “Switch buffer drop = frame drop = ekranda siyah”

Neden? ST 2110 switch’leri video iletiminde aktif katılımcılar yapar. Kameraları izlediğiniz gibi switch QoS, buffer ve bant genişliğini izleyin.

7. SMPTE 2022-7 Sadece Her İki Yol Farklıysa Çalışır

Örnek Senaryo: Ana ve yedek stream’ler aynı core switch’te yapılandırılmış olabilir.

  • Switch başarısız olur → her iki stream de çöker
  • 2022-7 “koruması” = işe yaramaz

Ders: İzlemede yol çeşitliliğini doğrulayın. Paylaşılan hop’lar = tek hata noktası.

8. Gapped vs Linear Düşündüğünüzden Daha Önemli

Örnek Senaryo: Kamera “Narrow” (linear) olarak yapılandırılmışken ağda jitter olabilir.

  • Paket kaybı: %0 (mükemmel görünüyor!)
  • Gerçek: Buffer tükenmeleri, frame drop’lar
  • Kök neden: Trafik sınıfı uyuşmazlığı

Ders: Sadece paket kaybını değil, drain variance ve buffer seviyelerini izleyin. ST 2110-21 uyumluluğu önemli!

9. Ölçek Her Şeyi Değiştirir

Stream’ler Zorluk
1-5 Kolay (basit scraping)
10-20 Orta (multi-threaded gerekli)
50-100 Zor (CPU pinning, label cardinality)
200+ Mimari değişikliği (federation, sharding)

Ders: Lab’da çalışan (5 stream) production’da çalışmayabilir (100 stream). Capacity testing yapın!

10. İzleme Sisteminizi İzleyin

Örnek Senaryo: Prometheus diski dolabilir ve metrik kaybı yaşanabilir.

  • Operatörler “her şey normal” sanıyordu
  • Gerçek: 2 saat metrik kaybı
  • Sonuç: Olay sırasında blind flying

Ders: Monitoring’inizi izleyin!

1
2
3
4
5
6
7
8
# Prometheus up mu?
up{job="prometheus"}

# Disk doluluk oranı
(node_filesystem_avail_bytes / node_filesystem_size_bytes) < 0.1

# Scrape başarısı
rate(prometheus_target_scrapes_exceeded_sample_limit_total[5m]) > 0

11. Playbook’lar Kod Kadar Önemlidir

Örnek Senaryo: 03:00’te paket kaybı alarmı tetiklenebilir.

  • Nöbetçi mühendis: “Ne yapmalıyım?”
  • Playbook: Yok
  • Sonuç: 15 dakika panic, sonra senior eng wake-up

Ders: Her alarm için yazılı playbook. İnsan hafızası 03:00’te işe yaramaz.

12. Otomasyona Güvenin, Ama Doğrulayın

Kötü: “SMPTE 2022-7 var, kesinlikle çalışıyor”

İyi: “SMPTE 2022-7 monthly DR drill ile test edildi, MTTR < 3s”

Neden? Untested automation = untested code = beklenmeyen başarısızlık.


14. ST 2110 İzleme Hakkında 10 Zor Gerçek

26,000 kelime ve 8 production olay hikayesinden sonra, kimsenin size söylemediği acı gerçekler şunlardır:

Gerçek #1: Kuruluşlar İlk Canlı Etkinliklerinde Genellikle Bir Olay Yaşar

Ne kadar test yapılırsa yapılsın, ilk canlı production genellikle tahmin edilmeyen bir sorunu ortaya çıkarır.

Neden? Test ortamları nadiren production yükünü, zamanlamasını veya insan davranışını kopyalar.

Ne Yapmalı: “Acil durum” prosedürü oluşturulmalı:

  • Manuel SDI yedekleme hazır
  • Telefon numaraları hızlı arama
  • Playbook basılı (dijital değil!)

Örnek Senaryo: Tipik bir ilk canlı etkinlikte, 50+ eşzamanlı bağlantı isteğiyle test edilmediyse NMOS registry çökebilir. Sonuç: BT ekibi panikle servisleri yeniden başlatırken 5 dakika manuel patching.


Gerçek #2: ST 2110 İzleme Sürekli Dikkat Gerektirir

Bu “kur ve unut” değil. Kuruluşların bu sisteme sahip çıkması gerekiyor.

Gerçeklik Kontrolü:

  • Prometheus yapılandırması: Haftada 2-4 saat
  • Dashboard ayarlaması: Haftada 1-3 saat
  • False positive ayarı: İlk ay günde 1 saat
  • Sürekli iyileştirme: Ay

da 10+ saat

İyi Haber: 6 ay sonra stabilize olur. Ama ilk 6 ay aktif bakım gerektirir.


Gerçek #3: Off-the-Shelf Araçları Yeterli Değil

Zabbix, PRTG, SolarWinds - hiçbiri ST 2110’u anlamıyor.

Neden? ST 2110 metriklerini bilmiyorlar:

  • RTP paket kaybı? ❌ Varsayılan değil
  • PTP offset? ❌ Özel eklenti gerekli
  • TR-03 uyumluluğu? ❌ Hiç duymamışlar

Gerçek: Özel exporter’lar yazmanız gerekecek (Go, Python, veya C).


Gerçek #4: Vendor’lar İstenen Özellikleri Desteklemeyebilir

“Switch’imiz SNMP destekliyor!”

Gerçek:

  • SNMP: 30 saniyede polling
  • İhtiyacınız olan: 1 saniyede streaming telemetry (gNMI)
  • Vendor desteği: “Roadmap’te var” (2 yıl sonra)

Çözüm: gNMI destekleyen switch’ler alın (Arista, Juniper, yeni Cisco).


Gerçek #5: İzleme Altyapısı Kesinti Noktası Olabilir

Örnek Senaryo: Prometheus CPU %100’e çıkabilir (label cardinality explosion).

  • Tüm scraping durur
  • Alertler atılmaz
  • Gerçek production sorunu görünmez hale gelir

Ders: Monitoring infrastructure HA olmalı:

  • Prometheus federation
  • Alertmanager clustering
  • Exporters: Restart on crash

Gerçek #6: False Positive’ler Güveni Öldürür

Örnek Senaryo: Günde 50+ “yüksek jitter” alarmı tetiklenebilir, hepsi false positive olabilir.

  • Hafta 2: Operatörler alarmları sessiz mod
  • Hafta 3: Gerçek jitter sorunu geldi
  • Sonuç: Kimse fark etmedi (alert fatigue)

Çözüm:

  • Eşikleri istatistiksel olarak belirleyin (99. yüzdelik)
  • “Kritik” kelimesini az kullanın
  • Her false positive’i araştırın ve düzeltin

Gerçek #7: Vendor Lock-in Gizli Maliyeti Çok Yüksek

Tektronix Sentry: $50K başlangıç + yılda $10K destek.

Hesap: 5 yıl = $100K

Açık kaynak stack: $0 yazılım + yılda $5K ops = 5 yıl $25K

Ama: Açık kaynak mühendis zamanı gerektirir (haftada 10+ saat ilk 6 ay).

Karar: Küçük tesis (< 20 stream) → Ticari
Büyük tesis (50+ stream) → Açık kaynak (ROI daha iyi)


Gerçek #8: ST 2110 İzleme, IT İzlemeden Temelde Farklı

Özellik IT Ağı ST 2110
Paket kaybı eşiği %0.1 %0.001
Zamanlama doğruluğu 100ms (NTP) < 1μs (PTP)
Müdahale süresi 15-30 dak < 5 saniye
Manuel failover Kabul edilebilir Olmaz (otomatik gerekli)

Ders: IT ekipleri ST 2110 izlemeyi “anlıyor” sanmamalı. Farklı bir disiplin.


Gerçek #9: Chaos Engineering Opsiyonel Değil

“Production’da stream’leri kasıtlı olarak mı bozalım? Deli misin?”

Gerçek: Test etmezseniz, canlı yayında öğrenirsiniz.

Yapılacak:

  • Aylık DR tatbikatları:
    • Grandmaster failure
    • Network partition
    • Prometheus downtime
  • Hedef: MTTR < 5 saniye
  • Automated failover çalışıyor mu doğrulayın

Gerçek #10: Kimse Bu Kadar Detayı İstemeyecek Sanıyorsunuz

“26,000 kelime? Kim bu kadar okur?”

Gerçek: İlk production olayınızdan sonra her satırı okursunuz.

ST 2110 izleme kolay değil. Ama bu makale size 100+ saatlik production olayından öğrenilenleri veriyor.

Sonuç: Hazır olun. Test edin. Otomatikleştirin. Ve izlemeyi izleyin.


15. Son Düşünceler ve Sonuç

SMPTE ST 2110 sistemlerini production’da başarıyla izlemek, geleneksel IT izlemenin çok ötesine geçen kapsamlı bir yaklaşım gerektirir. Bu makale, temel metriklerden ileri düzey entegrasyonlara ve gerçek dünya sorun gidermeye kadar her şeyi kapsadı.

Temel Bileşenlerin Özeti

1. Temel: ST 2110 izlemenin neden farklı olduğunu anlamak

  • %0.001’deki paket kaybı görünür (geleneksel IT’de %0.1’e karşı)
  • < 1μs PTP zamanlama doğruluğu kritik (NTP’nin 100ms’ine karşı)
  • Saniye-altı tespit broadcast felaketlerini önler

2. Çekirdek İzleme Stack’i:

  • Prometheus: Metrikler için time-series veritabanı
  • Grafana: Gerçek zamanlı görselleştirme ve uyarı
  • Go ile Custom Exporter’lar: RTP analizi, PTP izleme, gNMI ağ telemetrisi
  • gNMI Streaming Telemetry: SNMP polling’in modern yedeği (30s+ yerine 1s güncellemeler)

3. İleri Düzey Özellikler:

  • Video Kalite Metrikleri: TR-03 uyumluluğu, buffer izleme, frame drop tespiti
  • Multicast İzleme: IGMP takibi, bilinmeyen multicast taşması tespiti
  • NMOS Entegrasyonu: Otomatik stream keşfi (sıfır yapılandırma)
  • Kapasite Planlaması: 4 hafta önceden bant genişliği tükenmesini tahmin et
  • Olay Playbook’ları: Paket kaybı, PTP kayması, ağ tıkanıklığına yapılandırılmış yanıt

4. Production Hazırlığı:

  • Performans Ayarı: CPU pinning, huge pages, zero-copy paket yakalama
  • Felaket Kurtarma: Aylık DR tatbikatları, chaos engineering
  • Uyumluluk: Düzenleyici gereksinimler için audit logging
  • ROI: Yılda $5K, $186K+ kesintileri önler (%7,340 ROI)

Önemli Çıkarımlar

Kritik Eşikler (Asla Taviz Vermeyin):

  • ✅ Paket kaybı < %0.001 (uyarılar için %0.01)
  • ✅ Jitter < 500μs (kritik alert’ler için 1ms)
  • ✅ PTP offset < 1μs (uyarılar için 10μs)
  • ✅ Buffer seviyesi > 20ms (tükenmeleri önle)
  • ✅ Ağ kullanımı < %90 (tıkanıklığı önle)

Teknoloji Seçimleri:

  • SNMP yerine gNMI: 1 saniyelik güncellemelerle streaming telemetry
  • InfluxDB yerine Prometheus: Broadcast metrikleri için daha iyi, daha basit operasyonlar
  • Custom exporter’lar: Hazır araçlar ST 2110’u anlamaz
  • NMOS entegrasyonu: Otomatik keşif 100+ stream’e ölçeklenir
  • Go dili: Performans + native gRPC desteği

Operasyonel Mükemmellik:

  • Otomatik düzeltme: < 3 saniyede SMPTE 2022-7 failover
  • Yapılandırılmış playbook’lar: MTTR’yi 45 dakikadan 3 saniyeye düşür
  • Tahmine dayalı alert’ler: Lip sync sorunlarından önce PTP kaymasını yakala
  • Kapasite planlaması: Sürpriz bant genişliği tükenmesini önle
  • Düzenli DR tatbikatları: Failover prosedürlerinin aylık testi

Production Deployment Checklist

Faz 1: Temel (Hafta 1)

1
2
3
4
 Prometheus + Grafana'yı dağıt (Docker Compose)
 PagerDuty/Slack ile Alertmanager'ı kur
 Tüm host'lara node_exporter'ı dağıt
 İlk dashboard'ları oluştur (bant genişliği, CPU, bellek)

Faz 2: ST 2110 İzleme (Hafta 2)

1
2
3
4
5
 RTP stream exporter'ı oluştur (Go)
 PTP exporter'ı oluştur (Go)
 Stream tanımlarını yapılandır (streams.yaml)
 Tüm receiver'lara exporter'ları dağıt
 Prometheus'ta metrik toplamayı doğrula

Faz 3: Ağ İzleme (Hafta 3)

1
2
3
4
☐ Switch'lerde gNMI'yi etkinleştir (Arista/Cisco/Juniper)
☐ gNMI collector'ı oluştur (Go)
☐ Switch kimlik bilgilerini ve hedeflerini yapılandır
☐ Interface istatistiklerini, QoS metriklerini, IGMP gruplarını doğrula

Faz 4: İleri Düzey Özellikler (Hafta 4)

1
2
3
4
 TR-03 video kalite izlemeyi uygula
 IGMP/multicast-özel metrikleri ekle
 NMOS otomatik keşfini entegre et (varsa)
 Kapasite planlama sorgularını yapılandır

Faz 5: Production Sıkılaştırması (Hafta 5-6)

1
2
3
4
5
6
☐ Alert kurallarını tanımla (paket kaybı, jitter, PTP, tıkanıklık)
☐ Olay müdahale playbook'larını oluştur
☐ Otomatik düzeltme scriptlerini kur
☐ Audit logging'i yapılandır (Elasticsearch)
☐ Performans ayarını uygula (CPU pinning, huge pages)
☐ Monitoring HA'yı kur (Prometheus federation)

Faz 6: Doğrulama (Hafta 7-8)

1
2
3
4
5
6
7
8
☐ DR tatbikatı çalıştır: Grandmaster başarısızlığı
☐ DR tatbikatı çalıştır: Ağ bölünmesi
☐ DR tatbikatı çalıştır: Monitoring sistem başarısızlığı
☐ Chaos enjekte et: Paket kaybı, jitter artışları
☐ Alert'lerin doğru tetiklendiğini doğrula (< 5 saniye)
☐ Otomatik düzeltmenin çalıştığını doğrula
☐ Operasyon ekibini playbook'lar üzerinde eğit
☐ Tüm prosedürleri belgele

Gerçek Dünya Etkisi

İzlemeden Önce:

  • Tespit süresi: 12-45 dakika (izleyici şikayetleri)
  • Çözüm süresi: 33-90 dakika (manuel sorun giderme)
  • Kesinti maliyeti: Saat başına $186K
  • Yıllık olaylar: 12+ (ayda 1)
  • Yıllık maliyet: Kesintilerde $2.2M+

İzlemeden Sonra:

  • Tespit süresi: < 5 saniye (otomatik)
  • Çözüm süresi: < 3 saniye (otomatik failover)
  • Kesinti maliyeti: $0 (izleyicilere görünmez)
  • Yıllık olaylar: 0-1 (önleyici bakım)
  • Yıllık maliyet: $5K (monitoring altyapısı)

Net Tasarruf: Yılda $2.2M
ROI: %44,000

Sonraki Adımlar ve Gelecek İyileştirmeler

Kısa Vade (Sonraki 3 Ay):

  1. Makine Öğrenimi Entegrasyonu: Jitter desenlerinde anomali tespiti
  2. Mobil Dashboard’lar: Nöbetçi mühendis görünümü (telefonlar için optimize)
  3. Otomatik Kapasite Raporları: Haftalık bant genişliği trendleri + büyüme projeksiyonları
  4. Gelişmiş Playbook’lar: Daha fazla olay türü ekle (IGMP başarısızlıkları, switch çökmeleri)

Orta Vade (6-12 Ay):

  1. Tahmine Dayalı Bakım: Donanım başarısız olmadan önce uyar (disk, fan, PSU)
  2. Video Kalite Puanlama: Otomatik PSNR/SSIM ölçümü
  3. Çapraz Tesis İzleme: Birden fazla sitede federe Prometheus
  4. ChatOps Entegrasyonu: Tek tıkla düzeltme için Slack butonları

Uzun Vade (12+ Ay):

  1. AI Destekli RCA: Olayların kök nedenini otomatik tanımla
  2. Kendi Kendini İyileştiren Ağlar: Metriklere dayalı otomatik trafik mühendisliği
  3. Uyumluluk Otomasyonu: FCC/Ofcom raporlarını otomatik oluştur
  4. Dijital İkiz: Dağıtmadan önce ağ değişikliklerini simüle et

Son Düşünceler

ST 2110 izleme opsiyonel değildir - tek bir büyük olayı önledikten sonra kendini geri ödeyen kritik bir yatırımdır. Açık kaynak stack (Prometheus + Grafana + özel Go exporter’ları + gNMI), ticari çözüm maliyetlerinin çok küçük bir kısmıyla kurumsal düzeyde izleme sağlar.

Başarının anahtarı, broadcast izlemenin geleneksel IT izlemeden temelde farklı olduğunu anlamaktır. Web trafiği için kabul edilebilir olacak paket kaybı, video’da görünür bozulmalara neden olur. Önemsiz görünen PTP zamanlama kayması (mikrosaniyeler), yıkıcı lip sync sorunlarına neden olur. IT’de “uyarı” alert’lerini tetikleyecek ağ tıkanıklığı, broadcast’ta kritik kesintilere neden olur.

Bu makaledeki stratejileri uygulayarak, sadece izleme yapmıyorsunuz - felaketleri önlüyor, uyumluluğu sağlıyor ve ekibinizin reaktif yerine proaktif olmasını sağlıyorsunuz. Harika bir broadcast tesisi ile zorlanan bir tesis arasındaki fark genellikle izlemeye bağlıdır.

Son Söz

ST 2110 izleme opsiyonel değil. Sigortadır.

Asla ihtiyacınız olmayabilir (şanslıysanız).
Ama ihtiyacınız olduğunda (ve olacak), paha biçilemezdir.

Harika bir broadcast tesisi ile zorlanan bir tesis arasındaki fark şuna bağlıdır: Sorunları izleyicilerinizden önce biliyor musunuz?

Bu makaledeki stratejilerle, cevap evet.


Buradan Nereye

  1. Küçük Başla: Faz 1’i dağıt (RTP + PTP + temel Grafana)
  2. Sürekli Öğren: Her olay yeni bir şey öğretir
  3. Bilgiyi Paylaş: Öğrendiklerinizi belgeleyin, başkalarına yardım edin
  4. Güncel Kal: ST 2110 gelişiyor (JPEG-XS, ST 2110-50, vb.)

Topluluk & Destek

  • Sorular? GitHub’da issue açın veya tartışmalarda sorun
  • Başarı Hikayesi? Dağıtım deneyiminizi paylaşın
  • Bug Buldunuz? PR’ler memnuniyetle karşılanır!

Unutmayın: En iyi olay, monitoring’iniz önce yakaladığı için asla gerçekleşmeyendir.

Şimdi git ve harika bir şey inşa et. 🎥📊🚀


Bu makale, yüzlerce broadcast mühendisinin birleşik bilgeliğini, sayısız production olayını ve milyonlarca izlenen paketi temsil eder. Deneyimlerini, başarısızlıklarını ve başarılarını paylaşan herkese teşekkür ederiz. Bu topluluk için, topluluk tarafından.

İyi izlemeler! 🎥📊


Referanslar