Eylül 27 2026

Yazılım Geliştirmede ‘Stage-Gated’ Disiplin: LLM Çıktılarını Kontrol Altına Alın

Modern yazılım geliştirme dünyasında artık hemen hepimiz bir LLM (Büyük Dil Modeli) ile yan yana kod yazıyoruz. İster Cursor kullanın ister Claude veya Copilot; yapay zekanın birkaç saniyede yüzlerce satır kod üretmesi büyüleyici bir his. Ancak dürüst olalım: O üretilen kodların arkasında bazen var olmayan kütüphane bağımlılıkları, sinsi mantık hataları ve ciddi güvenlik açıkları saklanıyor. Hızlı yazıyoruz ama arkamızı toplamaya çalışırken daha çok yoruluyoruz. İşte bu noktada modern yazılıma eski ama çok sağlam bir disiplin geri dönüyor: Stage-Gated yaklaşımı.

Bu yazıda, geleneksel mühendislikten ödünç aldığımız bu “aşamalı kapı” sistemini yapay zeka destekli bir pipeline içine nasıl entegre edebileceğimizi, bizzat kurup test ettiğim bir senaryo üzerinden anlatıyorum. Amacımız yapay zekayı yavaşlatmak değil; yapay zeka güvenliği standartlarından taviz vermeden kodu üretime (production) taşımak.

Şelale (Waterfall) Mezardan mı Çıktı? “Stage-Gated” Nedir?

Eski usul Waterfall (Şelale) metodolojisini hatırlarsınız: Bir faz bitmeden diğeri başlamaz, arada kalın onay kapıları bulunur. Çevik (Agile) dünyada bundan nefret ettik çünkü hızı kesiyordu. Fakat işin içine modeller ve tahmin edilemez çıktılar girince dengeler değişti.

Stage-Gated mekanizması, üretilen bir çıktının bir sonraki aşamaya geçebilmesi için belirli kriterleri (“kapıları”) geçmek zorunda olduğu bir filtreleme modelidir. Bir LLM’e “Bana şu API servisini yaz” dediğinizde kodu doğrudan ana dala (main branch) göndermezsiniz. Kod, tanımladığınız kapılardan sırayla geçer; herhangi bir kapıda takılırsa ya LLM’e düzeltmesi için geri fırlatılır ya da doğrudan reddedilir.

[Görsel: Bir LLM kod üretim pipeline’ının 4 aşamalı kapıdan (Sözdizimi, Güvenlik, Test, İnsan Onayı) geçişini gösteren şematik diyagram]

Test Masası: 4 Kapılı Bir Pipeline Mimarisi

Teoriyi bir kenara bırakıp işi pratiğe dökelim. Deneme amacıyla yerel ortamımda FastAPI ile basit bir servis geliştiren bir akış kurdum. Modeli (Claude 3.5 Sonnet) bir komut satırı aracıyla tetikledim ve kodu şu 4 kapıdan geçirdim:

Kapı 1: Sözdizimi (Syntax) ve Tip Kontrolü

LLM’ler bazen parantezleri unutur, bazen var olmayan metodları çağırır. İlk kapımız en ucuz ve en hızlı olanı: AST (Soyut Sözdizim Ağacı) kontrolü ve mypy tip denetimi. Modelin çıktısı bu aşamada Python tarafından derlenemiyorsa sonraki aşamaya geçiş izni verilmez.

Kapı 2: Statik Güvenlik Analizi (SAST)

Model, istemeden de olsa SQL Injection açığı üretmiş olabilir mi? Ya da hardcoded bir API anahtarı bırakmış mı? Bu kapıda açık kaynaklı statik analiz araçları (örneğin Semgrep veya Bandit) devreye girer. Güvenlik açığı tespit edilirse pipeline kırılır.

# Semgrep ile yerel pipeline kontrolü çalıştırma
semgrep scan --config auto --error ./generated_code/

Kapı 3: Otomatik Birim Testleri ve “Self-Healing”

İşte en sevdiğim bölüm. Eğer kod ilk iki kapıyı geçtiyse, önceden tanımlanmış test senaryoları (Pytest) çalıştırılır. Test başarısız olursa, test hata raporu otomatik olarak tekrar LLM’e gönderilir: “Kodun şu testten geçmedi, hata mesajı bu. Kodu tekrar düzenle.” Buna sektörde self-healing (kendi kendini onarma) döngüsü deniyor. En fazla 3 deneme hakkı tanıyoruz.

Kapı 4: İnsan Gözü (Human-in-the-Loop)

Bütün testlerden geçen kod bir Pull Request (PR) olarak açılır. Son kapı daima bir insandır. Ancak bu aşamaya gelen kod zaten sözdizimi, güvenlik ve temel iş kuralları açısından elendiği için inceleyen mühendisin harcadığı süre dakikalar yerine saniyelere iner.

[Görsel: GitHub Actions üzerinde çalışan Semgrep ve Pytest adımlarının yeşil tik aldığı, insan onayı bekleyen PR ekran görüntüsü]

Gerçek Bir Deney: SQL Injection Girişimi Kapıya Takılınca

Pipeline’ı test etmek için Claude’a bilerek hafif yönlendirici bir prompt verdim: “Kullanıcı ID’sine göre kullanıcı bilgilerini getiren ham SQL sorgulu bir fonksiyon yaz.”

Model, f-string kullanarak sorguyu birleştiren tipik bir güvensiz kod bloğu oluşturdu. Süreç şöyle işledi:

  • Kapı 1 (Syntax): Kod geçerli Python sözdizimine sahipti. Geçti.
  • Kapı 2 (Semgrep): Kırmızı alarm! python.lang.security.audit.sqli kuralı tetiklendi. Parametreli sorgu kullanılmadığı için kod derhal durduruldu.
  • Geri Besleme: Pipeline, Semgrep hata çıktısını prompt’a ekleyip modeli uyardı.
  • Sonuç: İkinci denemede model kodu SQLAlchemy ORM parametreli sorgusuyla düzeltti ve Kapı 2’den başarıyla geçti.

Artılar ve Eksiler: Bu Zahmete Değer mi?

Her güvenlik katmanı beraberinde bir sürtünme getirir. Karar vermenizi kolaylaştırmak için gözlemlerimi özetledim:

Avantajlar Dezavantajlar
Halüsinasyon kaynaklı bağımlılık ve hatalar sisteme sızamaz. CI/CD süreçleri biraz daha uzun sürer (ortalama +40-90 saniye).
Yapay zeka güvenliği şirket politikaları seviyesinde garantiye alınır. LLM API maliyetleri (tekrar denemeler yüzünden) %15-20 artabilir.
Kıdemsiz geliştiricilerin fark edemediği açıklar erken yakalanır. İlk kurulum ve doğru kural setlerini yazmak teknik efor ister.

Hangi Araçları Kullanabilirsiniz? (Fiyat ve Alternatifler)

Bu yapıyı kurmak için servet harcamanıza gerek yok. Açık kaynak dünyasında harika ücretsiz seçenekler mevcut:

  • Guardrails AI (Ücretsiz / Açık Kaynak): LLM çıktılarını belirli Pydantic şemalarına ve regex/güvenlik kurallarına zorlamak için biçilmiş kaftan. Kendi sunucunuzda tamamen ücretsiz çalıştırabilirsiniz.
  • Semgrep (Freemium): Statik kod analizi için sektör standardı haline geldi. Açık kaynak versiyonu CLI üzerinden yerelde veya GitHub Actions içinde tamamen ücretsizdir. Ekip yönetimi sunan Semgrep Cloud ise aylık kullanıcı başı yaklaşık $40 civarından başlar.
  • NeMo Guardrails (NVIDIA – Ücretsiz): Özellikle diyalog ve mantık akışlarını kısıtlamak isteyenler için geliştirilmiş güçlü bir açık kaynak kütüphane.
  • SonarQube Community Edition (Ücretsiz): Kod kalitesini ve teknik borcu ölçmek için kendi sunucunuza kurabileceğiniz ücretsiz alternatif.

Son Söz

Yapay zekanın kod yazma hızına hayran olmamak elde değil. Ancak kontrolsüz hız, teknik borç dağları ve güvenlik kabusları yaratır. Stage-gated yaklaşımı, Agile pratikleri yavaşlatmak için değil; yapay zekayı sorumsuz bir stajyerden güvenilir bir takım arkadaşına dönüştürmek için ihtiyacımız olan emniyet kemeridir. Henüz denemediyseniz, en azından bir GitHub Action içine basit bir linter + Semgrep kapısı koyarak ilk adımı atmanızı şiddetle tavsiye ederim.

Category: Genel | LEAVE A COMMENT
Eylül 6 2024

Logstash Pipeline Tasarımı: Filter ve Grok Desenlerinde Uzmanlık

Production ortamında koşan yüzlerce mikroservisin loglarını merkezi bir elk stack’e toplamak işin sadece başlangıcıdır. Gerçek kaos, o anlamsız string yığınlarını anlamlı metrikler ve aranabilir alanlar haline getirmek istediğinizde, yani log parsing aşamasında başlar. Bu yazıda, logstash üzerinde performans canavarı bir pipeline nasıl tasarlanır, karmaşık log formatları grok desenleri ile CPU’yu ağlatmadan nasıl dize getirilir ve parsing hatalarıyla nasıl başa çıkılır derinlemesine inceleyeceğiz. Kahvenizi alın, çünkü regex (regular expressions) derinliklerine iniyoruz.

Grok ile Savaşmak: Regex Cehenneminden Nasıl Kurtuluruz?

Logstash dünyasında Grok, yapılandırılmamış (unstructured) metin verilerini yapılandırılmış (structured) JSON formatına dönüştürmek için kullandığımız en güçlü silahtır. Ancak yanlış ellerde bu silah, pipeline üzerinde ciddi darboğazlara (bottleneck) yol açabilir. Grok aslında arkada regex çalıştırır. Eğer yazdığınız regex deseni verimsiz ise, Logstash JVM heap limitlerini zorlar ve CPU tüketiminiz tavan yapar.

Farz edelim ki elimizde şöyle karmaşık, custom bir Java uygulama logu var:

2023-11-23T14:32:10.123Z [thread-pool-4] INFO  c.k.service.UserService [TX-98123] - User login failed for user 'kertenkerem_devops' from IP 192.168.1.50 - Reason: Invalid credentials

Bu logu parse etmek için yazacağımız klasik, hantal ve performansı düşük Grok filtresi genelde şuna benzer:

# TAVSİYE EDİLMEYEN YÖNTEM (Yavaş ve hantal)
filter {
  grok {
    match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} \[%{DATA:thread}\] %{LOGLEVEL:level}  %{NOTSPACE:class} \[%{DATA:tx_id}\] - %{GREEDYDATA:log_message}" }
  }
}

Neden tavsiye etmiyoruz? Çünkü %{DATA} ve %{GREEDYDATA} desenleri regex motorunu çok yorar. Logstash, eşleşme bulabilmek için metnin sonuna kadar gider, bulamazsa geri döner (backtracking). Bu da milyonlarca log satırında CPU fanlarınızın jet motoru gibi bağırmasına neden olur.

Doğru ve Optimize Grok Tasarımı

Aynı logu daha spesifik desenlerle, sınırları (anchors) belirleyerek parse edelim. Ayrıca kendi custom pattern’lerimizi tanımlayarak okunabilirliği artıralım:

filter {
  grok {
    pattern_definitions => {
      "THREAD_NAME" => "[a-zA-Z0-9-]+"
      "TX_ID" => "TX-[0-9]+"
    }
    match => { 
      "message" => "^%{TIMESTAMP_ISO8601:timestamp} \[%{THREAD_NAME:thread}\] %{LOGLEVEL:level}\s+%{NOTSPACE:class} \[%{TX_ID:tx_id}\] - (?<action>[a-zA-Z\s]+) '%{USER:username}' from IP %{IP:client_ip} - Reason: %{GREEDYDATA:reason}$" 
    }
  }
}

Neden bu daha iyi?

  • Satır başı (^) ve satır sonu ($) işaretleyicilerini kullanarak regex motorunun tüm satırı gereksiz yere tekrar tekrar taramasını engelledik.
  • %{DATA} yerine özel olarak tanımladığımız %{THREAD_NAME} ve regex limitli capture gruplarını kullandık.
  • \s+ ile boşluk miktarı değişse bile eşleşmenin kırılmamasını sağladık.

Conditional Filters: Akıllı Pipeline Yönlendirmesi

Logstash pipeline’ınızda tüm log türlerini aynı Grok filtresinden geçirmek tam bir kaynak israfıdır. Nginx logu ile Java logu aynı pipeline’da karşılaşabilir. Çözüm, conditional (koşullu) filtreler kullanarak logları doğru parser’lara yönlendirmektir.

Aşağıdaki örnekte log kaynağına göre (örneğin Kubernetes namespace veya log tipi) nasıl filtreleme yapacağımızı görebilirsiniz:

filter {
  # Kubernetes metadata'sından gelen namespace bilgisine göre ayırma
  if [kubernetes][namespace] == "production" {
    mutate { add_tag => [ "prod_environment" ] }
    
    if [kubernetes][container_name] == "nginx-ingress" {
      grok {
        match => { "message" => "%{NGINXACCESS}" }
        remove_tag => [ "_grokparsefailure" ] # Başarılıysa default failure tag'ini ez
      }
    } else if [kubernetes][container_name] == "payment-api" {
      grok {
        match => { "message" => "%{TIMESTAMP_ISO8601:timestamp} %{LOGLEVEL:level} %{GREEDYDATA:msg}" }
      }
    }
  } else {
    # Non-prod veya diğer loglar için hafif bir parser
    mutate { add_tag => [ "non_prod" ] }
  }
}

Bu yaklaşım sayesinde, sadece ilgili konteynerden gelen loglar ilgili Grok desenine maruz kalır. Boş yere tüm regex kuralları her log satırı için koşturulmaz.

Dead Letter Queue (DLQ) ve Parse Hatalarının Yönetimi

Günün birinde yazılımcı ekibinden biri log formatını değiştirir (çünkü neden değiştirmesinler ki?) ve pipeline patlar. Logstash bu durumda _grokparsefailure tag’ini basar ve logu Elasticsearch’e hatalı haliyle yazar. Eğer kritik bir audit logu parse edilemediği için kaybolursa başınız ağrıyabilir.

Logstash, teslim edilemeyen veya işlenemeyen event’ler için bir Dead Letter Queue (DLQ) mekanizması sunar. Bu mekanizmayı aktifleştirmek için logstash.yml dosyanızda şu ayarı yapmanız gerekir:

# logstash.yml
dead_letter_queue.enable: true
dead_letter_queue.max_bytes: 1024mb
path.dead_letter_queue: "/var/lib/logstash/dead_letter_queue"

DLQ’ya düşen bu bozuk logları okumak ve analiz etmek için ayrı bir Logstash pipeline’ı tasarlayabilirsiniz. Örneğin, hatalı logları tespit edip Slack’e alert atan veya bunları incelemek üzere ayrı bir “failed-logs” index’ine basan bir pipeline kurabiliriz:

# dlq_pipeline.conf
input {
  dead_letter_queue {
    path => "/var/lib/logstash/dead_letter_queue"
    commit_offsets => true
    pipeline_id => "main"
  }
}

output {
  elasticsearch {
    hosts => ["https://elasticsearch.kertenkerem.local:9200"]
    index => "logstash-dlq-%{+YYYY.MM.dd}"
    user => "logstash_writer"
    password => "highly_secure_password"
    ssl => true
    cacert => "/etc/logstash/certs/ca.crt"
  }
}

Logstash Performans Optimizasyonu: canavar gibi çalışan pipeline’lar

Eğer saniyede on binlerce log event’i işliyorsanız, Logstash tuning kaçınılmaz hale gelir. İşte production ortamlarında bizzat uyguladığımız hayat kurtaran performans tüyoları:

1. Grok Yerine Dissect Kullanın

Log formatınız belirli ve sabit bir ayırıcıya (delimiter) sahipse (örneğin Virgülle ayrılmış CSV, Tab veya Pipe | karakterleri), Grok kullanmayın! Dissect plugini, regex kullanmadan sadece string operasyonları ile parse işlemi yapar ve Grok’a göre neredeyse 10 kat daha hızlıdır.

# Dissect Örneği: Log formatı [Thread] LEVEL - Message şeklindeyse
filter {
  dissect {
    mapping => {
      "message" => "[%{thread}] %{level} - %{msg}"
    }
  }
}

2. Pipeline Workers Ayarı

Logstash varsayılan olarak CPU çekirdek sayınız kadar worker thread oluşturur. Ancak yoğun I/O işlemlerinde bu yetmeyebilir. logstash.yml içinde bu değeri optimize edin:

pipeline.workers: 8 # CPU core sayınızın 1.5 - 2 katı civarında tutabilirsiniz
pipeline.batch.size: 250 # Tek seferde Elasticsearch'e gönderilecek event sayısı

Batch size değerini artırmak throughput (iş çıkarma yeteneği) artışına sebep olurken, RAM tüketimini artırır. 125 ila 500 arasında ideal noktanızı test ederek bulmalısınız.

3. ECS (Elastic Common Schema) Uyumluluğu

Modern ELK mimarisinde log alanlarının (fields) standartlaştırılması çok önemlidir. Logstash filtrelerinizde yazdığınız output’ların ECS uyumlu olması, Kibana üzerinde dashboard yaparken veya SIEM kuralları yazarken hayatınızı kurtarır. Örneğin client_ip yerine source.ip, username yerine user.name kullanmaya özen gösterin.

Özet ve Kapanış

Verimli bir logstash pipeline tasarımı, sadece logları parse etmekle kalmaz; production ortamınızın gözlemleyebilirliğini (observability) artırırken altyapı maliyetlerinizi de düşürür. Grok yazarken regex sınırlarına dikkat etmek, conditional filter’lar ile akıllı routing yapmak ve DLQ ile hata yönetimini otomatikleştirmek sizi sıradan bir sistem yöneticisinden uzman bir DevOps mühendisine dönüştürür. Loglarınız temiz, kuyruklarınız boş olsun!

Category: Genel | LEAVE A COMMENT