Uygulama Loglarının Fluentd ile Toplanıp Merkezi Log Sistemine Aktarımı

Günümüzün çok parçadan oluşan sistemlerinde Elastic benzeri merkezi log sistemleri sektörel bir standart haline haline geldi. Mikroservis ve otomasyon dalgası öncesinde de büyük sistemler irili ufaklı bir sürü uygulamadan oluşuyordu, fakat insanlar ana uygulamanın sunucudaki log dosyalarını inceleyerek problem çözülüyorlardı. İhtiyaç olursa entegre olunan diğer uygulamalara dallanıp bakıyorlardı veya hatayı ilgili ekibe, servis sahibine iletiyorlardı. Mikroservis mimarisi ve sonrasında belki de serverless yaklaşımı; nesnelerden moleküllere moleküllerden atomlara doğru gidip, modülerlik ekseninde ibreyi sonsuza doğru çekti, eş zamanlı olarak yazılım sistemlerinde yıkıcı etkiler yarattı. Bu dalgadan log dünyası da nasibini aldı ve geri dönülmez bir şekilde değişti.
Her şey zıddıyla kaimdir denir. Bu kadar parçaladıktan sonra parçalardan anlamlı bir şey çıkarmak için geriye toplamak gerekiyor. Veri tabanında normalizasyon yaptıktan sonra sorgularken tabloları birleştirmek (join) gibi. Uygulama logları da böyle. İsteklerin onlarca uygulama arasında dolaşıp durduğu sistemlerde bu uygulamaların loglarını birleştirmek hata çözmek için zorunlu bir gereksinim oldu. Bu ihtiyaç merkezi log sistemlerinin gelişmesine, yeni modern ücretli veya ücretsiz ürünlerin ortaya çıkmasına, entegrasyonların kolaylaşmasına, insanların bu araçların sağladığı kolaylık, hız ve problem çözme gücüne alışmasına, alışkanlıkların değişmesine yol açtı. Artık kimse terminalden log “grep”lemek veya log dosyalarının satırlarında kaybolmak istemiyor. Birden fazla uygulama olduğunda bu yeterli de olmayabiliyor. Elasticsearch ve Kibana ikilisinin sunduğu, problem çözmede hayati rolü olan, hızlı ve kapsamlı arama, filtreleme ve gösterim gücü karşısında diğer seçenek taş devrinden kalma gibi duruyor.
İhtiyaç Ne?
Kibana arayüzünden log incelemenin kolaylığına alışan insanlara, onlarca servis olmasa bile git sunucuya gir, log dosyalarını al ve filtrele demek; taş devrine dön demek gibi oluyor. Hem canlı ortam sunucularına log almak için elini kolunu sallayarak girmek mümkün olmayabilir, ki olsa bile girmek istemeyebiliriz; hem de bu şekilde log incelemek efektif değil, bir web arayüzünden incelemeyle kıyaslandığında. Hatayı hızlı çözmek paha biçilmez bir şey. Ameleliği araçlara yüklersek, bize de hızlıca hatayı tespit etmek ve ilgili ekiplere haber vermek düşer, düşünmek ve karar vermek için zaman kalır. Üzerimizdeki baskı bir nebze hafifler. Hızlı ve kolay bir şekilde hatayı tespit etmek hem bizi hem de hata sebebiyle uygulamayı kullanamaz hale gelenleri mutlu eder. Maliyeti az, faydası çok. Tabi uygulamanın dili varsa ve söylenmesi gerekenleri söylüyorsa. Açık ve anlaşılır bir şekilde söylüyorsa.
Teknik İhtiyaç Ne?
Bir uygulama sunucu kümesi düşünün. Üzerlerinde Docker konteynırlar içinde çalışan uygulama modüllerimiz olsun. Bu uygulamalar geleneksel biçimde hem dosya sisteminde rotasyonlu bir şekilde log tutsun, hem de konteynırların ‘standart output’una yazsın detay logları, formatlı veya formatsız olarak. Yani hibrit bir log topluluğu olsun karşımızda. Böyle bir durumda sunuculardaki logları merkezi log sistemine aktarılmak üzere Kafka’ya nasıl iletebiliriz?
Hangi Aracı Kullanalım?
Çeşitli alternatifler mevcut. Bunlardan öne çıkan birkaçı Logstash, Filebeat ve Fluentd.
Filebeat az kaynak harcıyor, fakat log kayıtları işlenemiyor. Daha çok hiç işlemeden hızlıca az kaynakla log iletme amaçlı olarak kullanılıyor. Yani düz log satırlarını json’a dönüştürme, json logları işleme, alan ekleyip çıkarma, filtreleme işlerinin büyük çoğunu yapamıyor. Docker’ın varsayılan log driver’ı json-file. Yani logları diske json olarak yazıyor. Filebeat’e bu dizinleri ve ve başka log kaynaklarına ait log dosyalarını söylediğinizde alıp iletmeye başlıyor.
Logstash log işleyebiliyor fakat 600MB’nin üzerinde bellek tüketiyor. Docker tarafından da direk bir Logstash log driver’ı sunulmuyor. Gelf driver ile UDP üzerinden logları göndermek bir çözüm fakat Logstash durduğu anda log kaybı yaşanıyor. Diğer bir yöntem Filebeat kullanarak Logstash’a aktarmak. Fakat bu da parçalı bir çözüm.
Böyle bir durumda hem asenkron log gönderebilen bir Docker log driver’ı olması, hem az kaynak tüketmesi, hem de logları işleyebilmesi sebebiyle Fluentd aracı tercih edilebilir. Fluentd ile hem dosya sistemindeki rotasyona tabi tutulan log dosyalarını hem de Docker konteynır loglarını toplamak, kayıtların üzerinde işlem yapmak, filtreleme yapmak mümkün, ayrıca Kafka eklentisi ile Kafka’ya göndermek, diske yedekleme, veya ihtiyaç duyduğunuz başka sistemlere göndermek mümkün. Negatif tarafı ise Docker CE Fluentd log driver için dual loglama sunmuyor, yani “docker logs [container-id]” ile konteynır loglarını listelemez hale geliyoruz.
Çözüm Demosu
- Üstte kafkacat aracı ile Kafka log-messages topic’i ne gelen kayıtlar izleniyor.
- Sol altta docker-compose ile sistem bileşenleri (Zookeeper, Kafka, Fluentd, iki Httpd konteynırı) ayağa kaldırılıyor. Gerçi örnek projeye sonradan farklı ihtiyaçların pilotunu yapmak için başka bileşenler de eklenmiş durumda.
- Sağ alt kısımda ise ilk olarak ab aracı ile eş zamanlı 10 tane olacak şekilde 100 tane http isteği yapılıyor birkaç kez ve Docker konteynır loglarının aktarımı test ediliyor. İkinci olarak da echo komutuyla dosya sisteminde izlenen formatlı ve formatsız log dosyalarına besleme yapılıyor ve aktarımlar izleniyor.

Alttaki gösterimde performans, kaynak tüketimi ve dayanıklılık durumları test ediliyor.
- Önce Fluentd kapalıyken 10K istek yapılıyor ve Fluentd açıldığında logların aktarıldığı görülüyor.
- Ardından 100K istek yapılıyor ve aktarımın ortasında Fluentd kapatılıyor. İstekler bittikten sonra Fluentd açılıyor ve aktarım başlıyor.
- Aktarım devam ederken bir 100K istek daha yapılıyor ve işlem sonunda logların 2K kadarlık bir kısmının kaybolduğu görülüyor.
Önemli Prensipler
Herkes sonuna kadar okumayabilir blog yazısını. Bu sebeple önemli olanı başta söylemekten hareketle loglarla ilgili önemli gördüğüm bazı önemli prensipleri paylaşmak istiyorum, detaylara girişmeden önce.
- JSON formatında logla. Uygulamada loglama yaparken ya hep json olarak formatla veya opsiyonel olarak json formatlamayı sun. Log parse etmek ve bilgileri çıkarmak zor ve farklı uygulamalarda formatlar farklılaşınca işler zorlaşıyor. Birden fazla satırlı hata loglarını birleştirmek zorlayıcı oluyor. Bazen tamamen mümkün de olmayabiliyor. Uygulama katmanında JSON formatlı log basmak, işlemeyi çok kolaylaştırıyot.
- Loga gerekli tüm bilgileri koy. Ne kadar log toplarsan topla, elde ettiğin şey logların söylediği kadardır. Gidilen servisin bilgisi, cevabı, http durum kodları, süreler loga yazılmıyorsa loglardan öğrendiğin bilgi problem çözmeye yetmeyebilir.
- Loglara korelasyon bilgisi koy. Loglar arasında korelasyon yoksa (oturum ve istek bazında) elde ettiğin şey kimin neyle ilgili olduğu bazılarında belli olan bazılarında belli olmayan bir çuval dolusu log olabilir. Korelasyon konusuyla ilgili detaylı bir makale için bkz.
- Entegrasyon dayanıklılığını test et. Kafka bağlantısı koparsa ne oluyor. Fluentd kapanırsa ne oluyor. Uygulamalar açılıp kapanırsa ne oluyor. Aşırı yük binerse ne oluyor.
- JSON cevaplarda dinamik tip kullanma uyumluluk için. Bir alanın bir tipi olsun. Başka tipe sahip bir bilgiyi başka bir alanda ilet. Bu hem log örneğinde Elasticsearch’te statik tip beklentileriyle çatışıyor ve hataya yol açıyor. Hem de statik tipli dillerde gelen veriyi object gibi tipsiz bir tiple :) karşılamak zorunda bırakıyor.
Fluentd Temel Kavramları
Farklı araçlarda isimlendirmeler farklı olsa da ihtiyaçlar ortak. Fluentd tarafındaki temel kavramsal dünyanın ayakları ise şöyle:
- Logların toplanması, girdi kaynakları: source
- Logların işlenmesi: filter
- Logların yönlendirilmesi veya iletilmesi: match
- Farklı log kaynaklarını işleme kodlarının ayrıştırılması, kolay yönetim ve modülerlik: label
- Eklentiler
Bu temel ayaklar üzerinde dahili ve harici eklentiler yardımıyla bir şeyi farklı bir çok yoldan yapmak mümkün. Örneğin parse işlemini hem source, hem filter, hem de match kısmında yapmak mümkün. Parse işlemi için farklı yeteneklere sahip eklentileri kullanmak mümkün. Alt alta filtreler yazıp eşleşme kuralları üzerinden log kayıtlarını işlemek mümkün. Label kullanarak farklı isim uzayları oluşturup logları yönlendirmek ve işlemek mümkün. Yine label kullanarak farklı kanallardan işleyip sonunda ortak bir yerden dışarı vermek mümkün.
Örnek Uygulama Üzerinden Kullanım Senaryoları
Örnek uygulama Fluentd ile log aktarımında karşılaşılabilecek birkaç temel senaryonun nasıl çözülebileceğini ortaya koymayı amaçlıyor.

Docker Konteynır Konfigürasyonu
- 9 satırda log driver belirtiliyor.
- 11. satırda fluentd adresi belirtiliyor.
- 12 satırda bu konteynırdan giden logların nodejs1 olarak etiketlenmesi söyleniyor.
- 13. satır çok önemli!!. Loglar asenkron olarak gönderilsin deniyor. Bu ayar yapılmadığında fluentd durduğunda loglar gönderilemediğinde konteynırı aşağı çekiyor ve kapanmasına sebep oluyor.
- 14. satır Docker log driver’a yazamadığında uygulamayı bloke etmesin deniyor.
- 15. satırda buffer boyutu 4MB olarak ayarlanıyor bu örnekte.
Docker Fluentd Imajı ve Konfigürasyonu
- Baz imaj üzerine Fluentd imajında kullanılması istenen eklentiler/kütüphaneler ekleniyor.
- 7. satırda konfigürasyon dosyalarını içeren dizin bağlanıyor.
- 8. satırda takip edilecek log dosyalarının bulunduğu dizinler bağlanıyor.
- 9. satırda dosyaya yazılacak loglar ve buffer dosyalarının bulunduğu /data dizini bağlanıyor.
- Kafka adresi ve diğer port bilgilerini içeren ortam değişkenleri ayarlanıyor.
Log Kaynakların Okunması — fluent.conf
- Forward tipi source ile TCP üzerinden Docker konteynır logları dinleniyor.
- port satırında ENV ile ortam değişkenlerinden okunup ayarlanıyor
- Tail tipi source ile dosya sistemindeki dosyalar takip edilip, okunabiliyor.
- path satırı dosya yolunu ifade ediyor. * vb regex kullanılabiliyor dosya yolu ifade edilirken. Formata uyan tüm log dosyaları takip ediliyor. Takip edilmemesi gerekenler de exclude_path ile takip edilmemesi gerekenler belirtilebiliyor.
- pos satırı dosyadan okunan loglara ait son pozisyon bilgisinin hangi dosyada tutulacağını ifade ediyor. Fluentd kapatılıp açılırsa, konteynır silinirse logları tekrar göndermesin diye.
- path_key dosya yolunun log içerisinde hangi isimle konulacağını ifade ediyor.
- parse satırı logun parse edilme yöntemini ifade ediyor.
- tag satırı log kaydına etiket ekliyor, sonraki adımlarda işlerken kullanmak üzere.
Dosyadan Okunan Logların İşlenip Kafka’ya İletilmesi — fluent.conf
- filter file** ifadesi ile file ile başlayan etiketli loglar yakalanıyor, bu örnekte file.formatted ve file.unformatted
- 5. satırda ruby ile kaydın içinde string tipli bir msg alanı varsa ve uzunluğu 100 karakterden fazlaysa kırpılıyor.
- 3. satırda 5. satırdaki işlemi yapmak için oluşan _dummy_ alanı siliniyor.
- 13. satırda hostname log kaydına ekleniyor
- 14. satırda “fex” uygulama adı app ismiyle log kaydına ekleniyor.
- 15. satırda log kaydının etiketi tag ismiyle log kaydına ekleniyor.
- 16. satırda log kaydında msg isimli bir alan varsa bu alan string’e çevrilip msgStr ismiyle log kaydına ekleniyor.
- 17. satırda karışık bişeyler yapılıyor :). Log kaydında m diye bir nesne varsa, onun da içinde m2 tipinde bir nesne varsa onu json’a çevirip üzerine yazıyor.
- 23. satırda log kaydı KAFKA_OUTPUT etiketine yönlendiriliyor. Bu etiket içeriği en sonda açıklanıyor.
Docker Loglarının İşlenip Kafka’ya iletilmesi — fluent.conf
- 1. satırda filter web1 ile web1 etiketine sahip loglar işleniyor.
- 3. satırda parse edilecek logların bulunduğu alan ifade ediliyor. Bu örnekte Docker logları log alanında iletiliyor.
- 4. satırda apache2 log formatına göre parse edilerek json’a dönüştürülen bilginin hangi isimle log kaydına konulacağı ifade ediliyor.
- 5. satırda orijinal alanın kalması söyleniyor. Bu senaryoda “log” alanı.
- 6.–9. satırlar arası apache2 formatına göre loglar parse ediliyor.
- 10. satırda web11json olarak etiketleniyor.
- 16.-19. satırlar arası web1json logu zenginleştiriliyor.
- 22. satırda filter web2 ile web2 etiketli loglar yakalanıyor, sonrasında zenginleştiriliyor.
- 31. satırda match web* ile web1 ve web2 etiketli loglar yakalanıyor, bu adımda logların son işlenmiş halleri yer alıyor. Bu loglar relabel eklentisi ile KAFKA_OUTPUT etiketine yönlendiriliyor.
NodeJS Loglarının İşlenip Dosya Sistemine ve Kafka’ya Gönderilmesi — fluent.conf
- 1. satırda match nodejs* ile nodejs1 ve nodejs2 etiketli loglar yakalanıyor.
- 2. satırda detect_exception satırı ile birden fazla satırdan oluşan ve ayrı log kayıtları olarak gelen loglar birleştirilerek tek bir kayıt haline geliyor. Aksi halde tüm hataya ait exception stack trace satırlarının her biri ayrı loglar olarak geliyor ve gidiyor. Eğer uygulama katmanında json formatında log basılmışsa bu tarz dertler olmuyor.
- 5. satırda tespit edilecek hata logunun JS dilinden basılan hataya ait olduğu söyleniyor.
- 6. satır çok önemli!!! Birden fazla kaynaktan gelen logların toplanırken birbirine karışmaması için docker konteynır id bazlı ayrıştırılarak işlenmesi gerektiği söyleniyor. Bu satırı kaldırıp anlık iki ayrı konteynıra yüzlerce eş zamanlı istek yaparak hata bastırıp yüzlerce karışmayı görmek mümkün. :)
- 7. satırı en geç 0.5sn içinde elindekileri göndermesini söylüyor.
- 8. satırda loglar işlenmek üzere NODE_JS etiketine gönderiliyor.
- 2. satırda başlayan ve filter ** ile NODEJS_PROCESS etiketine (label) gelen tüm loglar grok parser ile özelden genele birkaç format ile parse edilmeye çalışılıyor.
- 25. satırda başlayan ve filter ** ile buraya gelen tüm loglar 500 karakterden uzunsa kırpılıyor. Ayrıca orijinal log kaydını içeren log alanı da siliniyor.
- 34. satırda başlayan match ** ile buraya gelen tüm loglar KAFKA_OUTPUT ve FILE_OUTPUT etiketlerine yönlendiriliyor, Kafka’ya ve dosya sistemine ayrı ayrı gönderilmek üzere.
Logların Kafka’ya Gönderilmesi — fluent.conf
- 3. satırda kafka_buffered tipinde bir Kafka gönderimi seçiliyor.
- 6. satırda ortam değişkenlerinden okunan Kafka broker’larının adresi ayarlanıyor.
- 10. satırda buffer tipinin dosya olduğu belirtiliyor, yani işlenen kayıtlar gönderilmek üzere önce diske yazılıyor. Ardından sırayla Kafka’ya gönderiliyor. Sıkıntı olması durumunda bekletiliyor, tekrar deneniyor, bu arada log kaybının önüne geçilmiş oluyor.
- 11. satır diskten kullanılacak buffer dosyasının dosya yolu belirtiliyor.
- 12. satırda 2sn’de bir Kafka’ya iletmesi söyleniyor.
- 15. satırda Kafka topic ismi belirtiliyor, logların hangi topic’e yazılacağı söyleniyor.
- 21. satırda Kafka’ya toplu (batch) olarak gönderilen logların toplam boyutu ifade ediliyor. Büyük olduğunda Kafka hata verebiliyor. Buradaki boyut bilgisi sıkıştırılmış haldeki boyut, açılınca ve metadata eklenince boyut daha da büyüyor. Bu örnekteki 500K açılınca 4/5MB haline geliyor olabilir. Bu ayar hata alınmaması için önemli.
- 22 satırda tekil bir mesajın maksimum uzunluğu byte cinsinden ifade ediliyor. Bu ayar da önemli. Aksi halde mesaj çok büyük olduğunda Kafka’daki ayara da bağlı olarak hata alınabiliyor. Bu örnekte 20KB’den büyük olan mesajları iletilmemesi ifade edilmiş oluyor. Bu mesajlar çöpe atılıyor. Not olarak Docker logları iletirken maksimum 16K’lık log kaydı iletiyor. Eğer bir log satırı 16K’dan daha büyükse dilimler halinde parçalanarak iletiyor.
- 23. satır maksimum 10 kez denemesini söylüyor. Denemeler eksponansiyel olarak artan aralıklarla yapılıyor.
Logların Dosya Sistemine Yazılması — fluent.conf
- 3. satırda logların dosyaya yazılacağı belirtiliyor.
- 4. satırda dosya yolu, formatı parametrik olarak belirtiliyor. Örneğin ../data/nodejs1/20210119/nodejs1-20210119–10.log.gz şeklinde.
- 5. satır gzip ile sıkıştırıp kaydetmesini söylüyor, 6. satır logları önceki dosyaya eklemesini.
- 11. satırda başlayan buffer kısmı buffer ayarlarını yapıyor. Loglar 1 dakikalık aralıklarla saatlik rotasyon yapılarak kaydediliyor.
- 18. satırda buffer dosya yolu veriliyor.
Fluentd Log Aktarım Metriklerinin Takibi — metrics.conf
Projede hem dahili monitor_agent eklentisi hem de prometheus eklentisi bulunuyor. Burada sadece monitor_agent’ı açıklamaya çalıştım.
- 3. satırda monitor_agent tip deklarasyonu yapılıyor.
- 5. satırda ortam değişkenlerinden açılacak port belirtiliyor.
- Metrikler http://localhost:24220/api/plugins.json üzerinden json formatında alınabiliyor, izleme araçlarına eklenip takip edilebiliyor, alarma tanımları yapılabiliyor.
Metrik örneği: Örnekte kafka_buffered ve file output bölümleri yer alıyor, diğer kısımları çıkardım. Kafka_buffered olanında retry bölümü dolu, 11 kez denediğini ve 12. kez denemek üzere beklediğini söylüyor. 38–42. satırlar. Bu Kafka kapalıyken alınmış bir metrik örneği. File olanında ise retry bölümü boş, en altta. Dosya sistemine yazarken bir hata almamış, yazabilmiş demek bu.
Bitirirken
Misafir umduğunu değil bulduğunu yermiş. Hepimiz birer misafiriz bulunduğumuz yerlerde. Genellikle aradıklarımızla değil bulduklarımızla yetiniyoruz. Onlar da bazen yakınsıyorlar bazense ıraksıyorlar. Bu aralar bizim menüdekilerden biri de buydu. Tarifini paylaşayım istedim. Faydalı olması dileğiyle.
Kaynaklar
- https://github.com/sfazilyesil/fluentd-example
- https://github.com/sfazilyesil/fluentd-example/releases/tag/1.0.0 (Sonradan değişme ihtimaline karşın blog yazısında kullanılan etiketli versiyon)
- https://medium.com/bili%C5%9Fim-hareketi/da%C4%9F%C4%B1t%C4%B1k-uygulamalar-d%C3%BCnyas%C4%B1nda-i%CC%87zleme-merkezi-log-sistemi-ve-i%CC%87z-s%C3%BCrme-8e62c138023b
- https://github.com/cloudboxlabs/blog-code/tree/master/distributed-logging
- https://medium.com/hackernoon/distributed-log-analytics-using-apache-kafka-kafka-connect-and-fluentd-303330e478af
- https://docs.fluentd.org/quickstart/life-of-a-fluentd-event
- https://docs.fluentd.org/quickstart
- https://www.fluentd.org/plugins
- https://docs.docker.com/config/containers/logging/fluentd/
- Ve diğer isimli/isimsiz yardım aldığım, yol gösteren, başlangıç ivmesini sunan değerli kahramanlar; blog yazarları, stackoverflow yazarları, forum yazarları, dokümantasyonlar. Meraklısı fluentd, docker, kafka kombinasyonlarıyla aratabilir, listedeki ilk yüzü kaynak listesine ekleyebiliriz. :)