Skip to content
Siber Vatan

Eklenti (Plugin) Sistemi ​

Muhammed Seyrek · Meriç Aytaş · Emrah Tusun

muhammedSeyrek/log-analyzer

Bir bilgisayar sistemi, transistörden uygulama eklentisine kadar tek bir bütün olarak tasarlanmaz. Her katman kendi altındaki katmanın ayrıntılarını gizler ve üstündeki katmana sınırlı, tanımlı bir arayüz sunar. İşletim sistemi işlemcinin elektriksel davranışını, uygulama disk denetleyicisinin komut setini, eklenti de uygulamanın iç veri yapılarını bilmek zorunda kalmaz.

Eklenti sistemi bu fikrin uygulama düzeyindeki karşılığıdır. Ana uygulama kararlı bir sözleşme tanımlar; eklentiler bu sözleşmeyi uygular; bir kayıt mekanizması da uygun eklentileri bulur, doğrular ve sisteme ekler. Bir aygıt sürücüsünün işletim sistemi çekirdeğine kayıt olması ile bir eklentinin uygulamaya kayıt olması benzer tasarım soruları doğurur: kararlı arayüz, sürüm uyumluluğu, yaşam döngüsü ve hata etkisinin sınırlandırılması. Bununla birlikte güvenlik ve hata yalıtımı bakımından aynı değildirler; çekirdek modülleri çekirdekle aynı ayrıcalık ve adres alanında çalışabilir.

Bu yaklaşımın değeri yalnızca kodu ayrı dizinlere bölmekten gelmez. İyi tasarlanmış bir eklenti sistemi aşağıdaki soruları açıkça yanıtlar:

  • Eklenti hangi arayüzü uygulamalıdır?
  • Eklentiler nasıl keşfedilir ve sisteme bağlanır?
  • Uyumsuz veya hatalı bir eklenti nasıl reddedilir?
  • Eklentinin başlatılması ve kapatılması nasıl yönetilir?
  • Bir eklenti hatasının ana uygulamayı etkilemesi nasıl sınırlandırılır?
  • Sözleşme değiştiğinde geriye dönük uyumluluk nasıl korunur?

Yazının uygulama bölümü, Go ile yazılmış log-analyzer projesindeki eklenti sistemini örnek alır. Bu yazıda işletim sistemi çekirdeği ile uygulama çekirdeği birbirinden ayrılır; “uygulama çekirdeği”, eklentiler dışında kalan temel uygulama ve CLI altyapısını ifade eder. Proje; log analizi, ağ telemetrisi ve akış işleme yeteneklerini uygulama çekirdeğine gömmek yerine aynı sözleşmeyi uygulayan üç eklenti olarak sunar.

Katmanlı Mimaride Eklentinin Yeri ​

Donanımdan uygulama eklentilerine uzanan yedi katman, eklenti sisteminin bütün içindeki yerini göstermek için kısa bir kavramsal model olarak kullanılabilir.

KatmanTemel SorumlulukSözleşme
1. Donanımİşlem, bellek, depolama ve giriş çıkış işlevlerini fiziksel olarak gerçekleştirir.Komut seti mimarisi
2. Donanıma yakın soyutlamaPlatforma özgü donanım ayrıntılarını ortak arayüzlerin arkasında toplar; konumu işletim sistemine göre değişir.HAL ve sürücü arayüzleri
3. İşletim sistemi çekirdeğiSüreçleri, belleği, dosya sistemlerini ve aygıt erişimini yönetir.Sistem çağrıları
4. Kullanıcı alanı kütüphaneleriUygulamaların tekrar kullandığı ortak işlevleri sağlar.Kütüphane API'si ve sembolleri
5. API ve ABIBileşenlerin kaynak kodu veya ikili düzeyde nasıl haberleşeceğini tanımlar.Sürümlenmiş arayüz tanımı
6. Uygulamaİş kurallarını ve kullanıcıya sunulan temel davranışları yürütür.Kullanıcı ve servis arayüzleri
7. EklentiUygulamanın eklenti API'sine uyarak yeni davranış ekler.Eklenti API'si

Bilgi

Bu yedi katman standartlaştırılmış bir referans modeli değildir ve tüm işletim sistemlerinde bire bir aynı dikey sırayı izlemez. Örneğin Windows HAL, çekirdeğin donanıma yakın alt katmanıdır; Linux aygıt sürücülerinin önemli bir bölümü ise çekirdek içinde çalışır. Sınıflandırma yalnızca ortak soyutlama fikrini göstermek için kullanılmıştır.

Donanımdan eklentiye yedi kavramsal katman; daha soyut yönü yukarı, Windows HAL işletim sistemi çekirdeğinin altında gösterilmiştir.

Donanımdan eklentiye kavramsal katmanlar. Katmanların konumu işletim sistemine göre değişir.

Donanım ​

En alt katmanda işlemci, bellek, depolama ve giriş çıkış denetleyicileri bulunur. Bu katmanın yazılıma verdiği söz komut seti mimarisidir. Komut seti aynı kaldığı sürece üretici iç tasarımı değiştirebilir; farklı önbellek düzenine sahip iki işlemci aynı ikili dosyayı çalıştırır.

USB, PCIe ve DDR5 gibi ortak arayüzler ile işlemci yükseltmesindeki soket, BIOS, yonga seti ve güç uyumluluğu koşulları.

Fiziksel bağlantı uyumu tek başına yeterli değildir; bileşen desteği ve platform gereksinimleri de denetlenir.

Donanım Soyutlama ​

Donanım soyutlama mekanizmaları platforma özgü ayrıntıları ortak arayüzlerin arkasında toplar. Windows'ta HAL, işletim sistemi çekirdeğinin altında ve donanıma yakın konumda kesme denetleyicisi, zamanlayıcı ve veri yolu gibi farklılıkları soyutlar. Linux'ta tek ve ayrı bir HAL katmanı yerine, bu sorumluluğun önemli bölümü işletim sistemi çekirdeği içindeki mimariye özgü kod ve aygıt sürücüsü modelleriyle karşılanır.

Windows'ta HAL'in işletim sistemi ile donanım arasındaki konumu ve Linux'ta mimariye özgü kod ile sürücülerin çekirdek içinde yer alması.

Donanım soyutlama mekanizmalarının Windows ve Linux'taki basitleştirilmiş görünümü.

İşletim Sistemi Çekirdeği ​

İşletim sistemi çekirdeği, donanıma erişimi HAL ve sürücüler gibi donanıma yakın bileşenler üzerinden denetler; kullanıcı alanına ise sistem çağrıları ve kütüphaneler aracılığıyla hizmet verir. Burada dış sözleşmeyle iç arayüzlerin kararlılığı aynı değildir. Linux, kullanıcı alanına sunduğu sistem çağrısı ABI'sini uzun süre korumayı hedeflerken çekirdek içi arayüzler için aynı kararlılık güvencesini vermez. Windows'ta ise doğrudan sistem çağrısı numaraları ve iç ayrıntılar sürümler arasında değişebildiğinden uygulamalar belgelenmiş Win32 ve benzeri API'leri kullanmalıdır. Eklenti tasarımı açısından ders şudur: dışarıya açılan sözleşme ile iç yapı aynı şey değildir ve ikisi aynı hızda değişmemelidir.

Yüklenebilir aygıt sürücüleri, eklenti mimarileriyle benzer kayıt ve yaşam döngüsü mekanizmaları kullanabilir. İşletim sistemi çekirdeğinin sürücü altyapısı belirli bir sürücünün ayrıntılarına değil, tanımlı sürücü arayüzlerine dayanır. Ancak çekirdek modülü çoğu sistemde işletim sistemi çekirdeğiyle aynı adres alanı ve ayrıcalık düzeyinde çalıştığı için bir hata tüm sistemi çökertebilir; burada süreç düzeyinde hata yalıtımı yoktur.

Uygulamaların işletim sistemi hizmetleri üzerinden donanıma erişimini ve yüklenebilir Linux modüllerinin hata sınırlarını gösteren şema.

İşletim sistemi hizmetlerine erişimin basitleştirilmiş görünümü. macOS'un çekirdeği XNU'dur; Darwin ise işletim sistemi bileşenlerini kapsar. Apple XNU açıklaması

Kullanıcı Alanı Kütüphaneleri ​

Kullanıcı alanında, uygulamaların ortak işlevler için yeniden kullandığı kütüphaneler bulunur. Paylaşılan kütüphanelerin dinamik yüklenmesi, eklenti gerçekleştirimlerinde kullanılabilen tekniklerden yalnızca biridir; her dinamik kütüphane yüklemesi kendiliğinden keşif ve kayıt içeren bir eklenti sistemi oluşturmaz.

Windows .dll, Linux .so ve macOS .dylib paylaşılan kütüphaneleri ile uygulamaların ortak işlevleri kullanması.

Paylaşılan kütüphaneler ortak kodun yeniden kullanımını sağlar; dinamik yükleme tek başına bir eklenti sistemi değildir.

API ve ABI ​

API, bileşenlerin kaynak kodu düzeyinde nasıl haberleşeceğini tanımlar: fonksiyon adları, parametreler, dönüş değerleri, hata davranışı. ABI ise aynı sorunun ikili düzeydeki karşılığıdır: çağrı kuralları, veri hizalaması, yapı düzeni, sembol adlandırma.

Aradaki fark pratikte çoğu zaman şuna karşılık gelir: kaynak düzeyindeki uyumsuz bir API değişikliği yeniden derlemede hata verir; ABI uyumsuzluğu ise önceden derlenmiş bileşen yüklenirken veya çalışırken ortaya çıkabilir. Örnekteki sistemde eklentiler ana uygulamayla birlikte derlendiği için ayrı dağıtılan ikili eklentilere özgü ABI uyumluluğu sorunu büyük ölçüde ortadan kalkar. Eklentilerin ayrı dağıtıldığı sistemlerde ise sürümleme ve uyumluluk politikası yazılı olmak zorundadır.

İşlem, girdi ve çıktı üzerinden API sözleşmesi örneği; Windows API, POSIX, REST ve Google Maps API kavramlarının ayrımı.

API bir sözleşme sunar. REST ise web API tasarımında kullanılan bir mimari stildir; bir standart adı değildir. Fielding'in REST tanımı

Uygulama ve Eklenti ​

Uygulama katmanı iş kurallarını yürütür ve alt katmanların yeteneklerini belirli bir amaca göre düzenler. En üstte ise uygulamanın eklenti API'sine uyarak yeni davranış ekleyen bileşenler bulunur.

Chrome, Photoshop, VS Code ve WordPress gibi uygulamaların farklı kullanıcı ihtiyaçları için eklenti sözleşmeleri üzerinden genişletilmesi.

Eklenti altyapısı, isteğe bağlı yeteneklerin ayrı bileşenlerde geliştirilmesini destekler.

Buradaki bağımlılık yönü dikkat çekicidir. Eklenti, ana uygulamanın somut iç yapısına değil ortak eklenti sözleşmesine bağımlıdır. CLI bağlama kodu da somut eklenti tiplerini bilmez; ancak uygulamanın birleştirme noktası olan plugins toplama paketi, üretilen import listesi üzerinden eklenti paketlerini bilir. Bu ayrım, olağan bir eklenti eklemesinde uygulama çekirdeğinin iş mantığını değiştirmeden sistemi genişletmeyi sağlar.

Katmanlar Arasındaki Ortak Fikir ​

Soruİşletim sistemi çekirdeği modülüUygulama eklentisi
Arayüz nedirÇekirdek modül API'siPlugin arayüzü
Nasıl bulunurModül dizini ve yapılandırmaDizin taraması ve kod üretimi
Nasıl kaydedilirKayıt fonksiyonlarıplugin.Register()
Uyumluluk nasıl denetlenirSürüm ve sembol denetimiDerleme anında arayüz uyumu
Yaşam döngüsüYükleme ve kaldırmainit() ve komut yürütme
Hata sınırıÇekirdek modülünde doğal süreç yalıtımı yoktur; hata sistemi etkileyebilirHata dönüşü; güçlü yalıtım için ayrı süreç veya servis

Eklentiden donanıma aynı yönde sıralanan katmanlar ve bu düzeylerde kullanılan farklı sözleşme veya modülerlik mekanizmaları.

Katmanlar aynı mekanizmayı kullanmaz; karşılaştırma ortak modülerlik fikrini gösterir. Uygulama düzeyindeki olağan genişletmeler özellikle Open/Closed ilkesiyle ilişkilidir.

Eklenti sistemi bu nedenle yeni bir fikir değil, işletim sistemi tasarımında uzun süredir kullanılan bir yaklaşımın uygulama düzeyine taşınmış halidir.

Eklenti Sistemi Nedir ​

Eklenti, ana uygulamanın tanımladığı sözleşme üzerinden çalışan ve belirli ölçüde bağımsız geliştirilebilen bir yazılım bileşenidir. Bu örnekte sözleşme internal/plugin altında bulunduğundan Go'nun internal görünürlük kuralı, eklentileri aynı üst modül ağacıyla sınırlar; ayrı bir depoda doğrudan içe aktarılarak geliştirilmeleri mümkün değildir. Eklenti yeni bir komut, veri dönüştürücü, doğrulama kuralı, çıktı biçimi veya entegrasyon sağlayabilir.

Kodu ayrı bir dizine koymak tek başına eklenti sistemi oluşturmaz. Tam bir eklenti sistemi en az şu bileşenleri içerir:

BileşenGörev
Eklenti API'siEklentinin sağlaması gereken davranışları ve veri biçimlerini tanımlar.
Keşif mekanizmasıKullanılabilir eklentileri bulur.
Yükleyici veya bağlayıcıEklenti kodunu sisteme dahil eder ve nesnesini oluşturur.
DoğrulayıcıEklentinin sözleşmeye ve desteklenen sürüme uyduğunu denetler.
Kayıt defteri (registry)Kayıtlı eklentileri kimlikleriyle saklar.
Yaşam döngüsü yöneticisiBaşlatma, çalıştırma, durdurma ve kaynak temizleme işlemlerini koordine eder.
Hata ve güvenlik sınırıEklenti hatalarının ve yetkilerinin etkisini sınırlar.

Çalışma Akışı ​

Bir eklentinin ana uygulamaya katılması genellikle aşağıdaki sırayı izler:

  1. Eklenti kaynakları taranır ve kullanılabilir eklentiler belirlenir.
  2. Bulunan eklenti kodu sisteme dahil edilir.
  3. Eklenti nesnesi oluşturulur ve kayıt defterine kaydedilir.
  4. Kimlik, sürüm ve gerekli metotlar doğrulanır.
  5. Ana uygulama kayıtlı eklentileri kendi akışına bağlar.
  6. Uygulama bir olay veya işlem isteğini ilgili eklentilere iletir.
  7. Hatalar eklenti sınırında kaydedilir ve politika gereği izole edilir.
  8. Uygulama kapanırken eklenti kaynakları serbest bırakılır.

Chrome, WordPress, VS Code ve Minecraft eklenti ekosistemleri ile sözleşme, geliştirme, keşif ve doğrulama, bağlama aşamaları.

Eklenti ekosistemleri ve genel katılma akışı. Keşif, kayıt ve yükleme zamanı mimariye göre değişir; Go örneğinin aşamaları aşağıda ayrıca açıklanır.

Keşif, bağlama ve çalıştırma ayrı aşamalar olarak ele alınmalıdır. Bu ayrım, eklenti bulunamadığında oluşan hatayla eklenti çalışırken oluşan hatanın farklı biçimde yönetilmesini sağlar.

Eklenti API'si Tasarımı ​

Eklenti API'si, eklenti sisteminin en önemli parçasıdır. Sözleşme çok dar olursa eklentiler gerekli işlemleri yapamaz; çok geniş olursa uygulamanın iç ayrıntıları dışarı sızar ve API'nin değiştirilmesi zorlaşır.

İyi bir eklenti API'si şu özelliklere sahip olmalıdır:

  • Eklentinin kullanabileceği veri ve işlemleri açıkça tanımlamalıdır.
  • Uygulamanın iç nesnelerini doğrudan paylaşmak yerine sınırlı bir bağlam sunmalıdır.
  • Girdi, çıktı ve hata davranışlarını belgelemelidir.
  • Bir sürüm bilgisi içermelidir.
  • Geriye dönük uyumluluk politikasını belirtmelidir.
  • Eklenti kimliğinin benzersiz olmasını sağlamalıdır.

log-analyzer projesinde sözleşme üç metottan oluşur ve internal/plugin/plugin.go dosyasında tanımlanır.

go
package plugin

import "github.com/spf13/cobra"

type Plugin interface {
	Name() string

	Description() string

	Command() *cobra.Command
}

Sözleşmenin bu kadar dar olması bilinçli bir tercihtir. Ana uygulama eklentiden yalnızca üç şey ister: benzersiz bir ad, kullanıcıya gösterilecek bir açıklama ve CLI'ye eklenecek bir komut ağacı. Eklentinin logları nasıl ayrıştırdığı, hangi dosyaları okuduğu veya ağdan ne topladığı uygulama çekirdeğini ilgilendirmez.

Go'da arayüz uyumu derleme anında denetlenir. Gerekli metotlardan birini sağlamayan bir tip Register() çağrısına geçemez; hata çalışma anında değil, derlemede ortaya çıkar.

Kayıt Defteri (Registry) ​

Kayıtlı eklentiler internal/plugin/register.go içindeki kayıt defterinde tutulur.

go
package plugin

import (
	"fmt"
	"sort"
	"sync"
)

var (
	registry = make(map[string]Plugin)
	mu       sync.Mutex
)

func Register(p Plugin) {
	mu.Lock()
	defer mu.Unlock()

	name := p.Name()
	if name == "" {
		panic("plugin: Register called with empty name")
	}
	if _, exists := registry[name]; exists {
		panic(fmt.Sprintf("plugin: Register called twice for %q", name))
	}
	registry[name] = p
}

func All() []Plugin {
	mu.Lock()
	defer mu.Unlock()

	names := make([]string, 0, len(registry))
	for name := range registry {
		names = append(names, name)
	}
	sort.Strings(names)

	plugins := make([]Plugin, 0, len(names))
	for _, name := range names {
		plugins = append(plugins, registry[name])
	}
	return plugins
}

func Get(name string) (Plugin, bool) {
	mu.Lock()
	defer mu.Unlock()
	p, ok := registry[name]
	return p, ok
}

Üç ayrıntı tasarım açısından önemlidir.

Birincisi doğrulama. Boş ve tekrarlanan eklenti kimlikleri kabul edilmez. Ancak bu kontrol tek başına CLI komut çakışmasını önlemez: kayıt anahtarı Name() değeridir, Cobra ise Command().Name() değerini kullanır. Bu nedenle bağlama aşaması, eklenti kimliğiyle komut adının eşleştiğini ve komut adının hem diğer eklentilerle hem de list gibi yerleşik komutlarla çakışmadığını ayrıca denetlemelidir.

İkincisi hatanın zamanı. Geçersiz bir kayıt için Register() çağrısı panic ile sonuçlanır. Bu, çalışma anında kullanıcı girdisiyle tetiklenebilecek bir hata olsaydı yanlış bir tercih olurdu. Burada kayıt kümesi kod üretiminden sonra bellidir ve kayıt program başlarken yapılır; dolayısıyla hata, yürütülebilir dosyanın ilk çalıştırıldığı anda ve kesin biçimde ortaya çıkar. Eklentilerin kullanıcı tarafından çalışma anında yüklendiği bir sistemde ise aynı durum hata değeri döndürmeyi gerektirir.

Üçüncüsü sıralama. All(), eklentileri ada göre sıralayarak döndürür. Go belirtimi paket başlatma sırasını bağımlılıklar ve paket yolları üzerinden tanımlasa da bu sıra kullanıcı arayüzü sözleşmesi olarak görülmemeli ve eklentiler arası davranış için kullanılmamalıdır. Açık sıralama sayesinde list komutunun çıktısı ve komutların eklenme sırası belirleyici olur.

Mutex kullanımı da kayıt defterini ileride eşzamanlı okuma yapan bileşenlere karşı korur.

Kendini Kaydeden Eklenti ​

Her eklenti kendi paketinde sözleşmeyi uygular ve init() içinde kendini kaydeder. Aşağıda log analizi eklentisinin tanımı yer alır.

go
package loganalyzer

import (
	"log-analyzer/internal/plugin"

	"github.com/spf13/cobra"
)

type LogAnalyzerPlugin struct{}

func init() {
	plugin.Register(&LogAnalyzerPlugin{})
}

func (p *LogAnalyzerPlugin) Name() string {
	return "loganalyzer"
}

func (p *LogAnalyzerPlugin) Description() string {
	return "Analyzes system logs and detects security threats"
}

func (p *LogAnalyzerPlugin) Command() *cobra.Command {
	return newRootCommand()
}

Command() metodu eklentinin tüm komut ağacını döndürür. Eklentinin iç yapısı bu noktadan sonra kendi sorumluluğundadır.

go
func newRootCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "loganalyzer",
		Short: "Log analysis and threat detection tool",
	}

	cmd.AddCommand(newStaticCommand())
	cmd.AddCommand(newLiveCommand())
	cmd.AddCommand(newInteractiveCommand())

	return cmd
}

Projedeki üç eklenti aynı sözleşmeyi uygular ancak tamamen farklı işler yapar:

EklentiAçıklamaAlt komutlar
loganalyzerSistem loglarını analiz eder ve güvenlik tehditlerini tespit eder.static, live, interactive
netmetricsScapy ile paket yakalar ve 5G telemetrisini UDP üzerinden iletir.capture, gen5g, collect
streamprocessorSınırlı bellekle gerçek zamanlı akış ayrıştırma ve filtreleme yapar.run, gen

CLI'nin genel bağlama kodunda bu adların hiçbiri geçmez. Buna karşılık birleştirme noktası olan üretilmiş plugins_gen.go dosyası, bu paketlerin import yollarını açıkça içerir.

Keşif ve Kod Üretimi ​

Go'da bir paketin init() fonksiyonunun çalışması için o paketin yürütülebilir dosyaya dahil edilmesi gerekir. Hiçbir yerden referans verilmeyen paket derlemeye girmez, dolayısıyla kendini kaydedemez. Projede bu sorun, eklenti dizinlerini tarayan ve boş tanımlayıcılı (blank) import listesi üreten bir araçla çözülür.

Proje Yapısı ​

text
log-analyzer/
├── cmd/
│   └── main.go
├── internal/
│   ├── cli/
│   │   └── root.go
│   └── plugin/
│       ├── plugin.go
│       └── register.go
├── plugins/
│   ├── doc.go
│   ├── plugins_gen.go
│   ├── loganalyzer/
│   ├── netmetrics/
│   └── streamprocessor/
└── tools/
    └── gen/
        └── main.go

Keşif Aracı ​

tools/gen, plugins dizinindeki alt dizinleri tarar. Nokta veya alt çizgi ile başlayan dizinleri atlar, en az bir .go dosyası içerenleri eklenti kabul eder ve sonucu sıralar.

go
func discoverPlugins(root string) ([]string, error) {
	entries, err := os.ReadDir(root)
	if err != nil {
		return nil, fmt.Errorf("read dir %q: %w", root, err)
	}

	var plugins []string
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		name := entry.Name()
		if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
			continue
		}

		hasGo, err := dirHasGoFile(filepath.Join(root, name))
		if err != nil {
			return nil, err
		}
		if !hasGo {
			continue
		}

		plugins = append(plugins, name)
	}

	sort.Strings(plugins)
	return plugins, nil
}

Üretilen dosya gofmt ile biçimlendirilip diske yazılır.

go
formatted, err := format.Source(buf.Bytes())
if err != nil {
	return fmt.Errorf("gofmt generated code: %w", err)
}

if err := os.WriteFile(path, formatted, 0644); err != nil {
	return fmt.Errorf("write %q: %w", path, err)
}

Üretim Noktası ​

plugins/doc.go hem toplama noktasını hem de üretim komutunu tanımlar.

go
// Package plugins is the aggregation point for all plugin modules.
// Each plugin lives in its own subdirectory and registers itself via
// init(); this package imports them all via blank imports so their
// init() functions run when the binary starts.
//
// The list of imports is generated automatically by tools/gen. Do not
// edit plugins_gen.go by hand.
package plugins

//go:generate go run ../tools/gen/main.go

Üretilen dosyanın işlevsel içeriği yalnızca blank import'lardan oluşur. Aşağıdaki gösterimde, üretim yorumunun paket dokümantasyonuna karışmaması için package plugins öncesine boş satır eklenmiştir. Aynı biçimin projede de elde edilmesi için kod üreticisi bu boş satırı yazmalıdır.

go
// Code generated by tools/gen; DO NOT EDIT.
// Re-run with: go generate ./...

package plugins

import (
	_ "log-analyzer/plugins/loganalyzer"
	_ "log-analyzer/plugins/netmetrics"
	_ "log-analyzer/plugins/streamprocessor"
)

Alt çizgi ile yapılan import, paketten hiçbir sembol kullanılmadığını ancak paketin yine de yürütülebilir dosyaya dahil edilip init() fonksiyonunun çalıştırılmasını istediğimizi belirtir.

Yeni bir eklenti eklemek için plugins/ altına dizin açmak, sözleşmeyi uygulamak ve üretimi tekrar çalıştırmak yeterlidir.

bash
make generate   # plugins/plugins_gen.go dosyasını yeniden üretir
make build      # önce generate çalışır, sonra yürütülebilir dosya derlenir

İpucu

Yeni eklenti eklendikten sonra kod üretimi çalıştırılmazsa derleme başarılı olur ancak eklenti list çıktısında görünmez. Paket hiçbir yerden import edilmediği için yürütülebilir dosyaya girmemiş, dolayısıyla init() hiç çalışmamıştır. Bu sessiz durumu önlemek için build hedefi generate hedefine bağlanmıştır.

Ana Uygulamanın Bağlanması ​

Giriş noktası yalnızca iki şey yapar: eklenti toplama paketini blank import ile dahil eder ve CLI'yi çalıştırır.

go
package main

import (
	"log-analyzer/internal/cli"

	_ "log-analyzer/plugins"
)

func main() {
	cli.Execute()
}

main() çalışmaya başlamadan önce tüm init() fonksiyonları tamamlanır. Bu nedenle CLI kurulurken kayıt defteri doludur.

go
func bindPlugins(root *cobra.Command) {
	seen := map[string]string{"list": "yerleşik komut"}
	for _, p := range plugin.All() {
		cmd := p.Command()
		if cmd == nil {
			panic(fmt.Sprintf("plugin %q returned a nil command", p.Name()))
		}
		commandName := cmd.Name()
		if commandName != p.Name() {
			panic(fmt.Sprintf(
				"plugin name %q does not match command name %q",
				p.Name(), commandName))
		}
		if owner, exists := seen[commandName]; exists {
			panic(fmt.Sprintf(
				"command %q conflicts with %s", commandName, owner))
		}
		seen[commandName] = fmt.Sprintf("plugin %q", p.Name())
		root.AddCommand(cmd)
	}
}

func newListCommand() *cobra.Command {
	return &cobra.Command{
		Use:   "list",
		Short: "List all registered plugins",
		Run: func(cmd *cobra.Command, args []string) {
			plugins := plugin.All()
			if len(plugins) == 0 {
				fmt.Println("No plugins registered.")
				return
			}
			fmt.Printf("Registered plugins: %d\n\n", len(plugins))
			for _, p := range plugins {
				fmt.Printf("  - %-20s %s\n", p.Name(), p.Description())
			}
		},
	}
}

func Execute() {
	root := newRootCommand()
	root.AddCommand(newListCommand())
	bindPlugins(root)

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}

Önerilen bindPlugins fonksiyonu somut eklenti adlarını bilmez. Döngü kayıt defteri üzerinde ilerler, kimlik ile komut adını karşılaştırır, list adıyla ve diğer eklentilerin kök komut adlarıyla çakışmaları reddeder ve her eklentinin komut ağacını kök komuta ekler. Yeni bir eklenti eklendiğinde bu genel bağlama mantığına somut eklentiye özel kod eklemek gerekmez.

list komutu ise sistemin kendini kısmen anlatmasını sağlar. Kullanıcı hangi eklentilerin ve açıklamalarının derlendiğini çalıştırma anında görebilir; bu örnek sürüm veya sağlık bilgisi göstermez.

Derleme Anı ve Çalışma Anı Keşfi ​

Bu tasarımda keşif, kod üretimi sırasında yani derlemeden önce; kayıt ise süreç başlarken gerçekleşir. Eklentiler çalışma anında dosya sisteminden okunmaz.

AşamaNe olur
Kod üretimiplugins/ taranır, import listesi yazılır
DerlemeEklenti paketleri yürütülebilir dosyaya dahil edilir, arayüz uyumu denetlenir
Süreç başlangıcıinit() fonksiyonları Register() çağırır
main()All() ile kayıt defteri okunur, komutlar bağlanır
Komut yürütmeYalnızca kullanıcının seçtiği eklentinin komutu çalışır

Bu yaklaşımın bedeli, yeni eklenti için yeniden derleme gerekmesidir. Karşılığında üç kazanç elde edilir: arayüzü uygulamayan eklenti derleme hatası verir, Go uygulamasının kendisi tek bir yürütülebilir dosya olarak dağıtılabilir ve çalışma anında yeni Go kodu yükleme yüzeyi oluşmaz. Bununla birlikte netmetrics özelliği Python yorumlayıcısı ve Scapy gibi harici çalışma zamanı bağımlılıkları gerektirdiğinden sistemin tamamı koşulsuz olarak “tek dosyalık dağıtım” değildir.

Go'nun standart kütüphanesindeki plugin paketi çalışma anında paylaşılan nesne yüklemeye izin verir, ancak yalnızca belirli platformlarda çalışır. Güvenilir birlikte çalışma için ana uygulama ve eklentiler aynı araç zinciri sürümü, derleme etiketleri ve ilgili derleme ayarlarıyla üretilmeli; ortak bağımlılıklar da aynı kaynak kodundan derlenmelidir. Bu kısıtlar, bazı dağıtım senaryolarını zorlaştırabilir. Go plugin paketi belgeleri

Çalışma anında yükleme yapan dillerde ise keşif ve doğrulama uygulama veya kullanılan eklenti altyapısı tarafından yönetilir. Python'da benzer bir sistem, modülleri tarayan ve her modülden bir fabrika fonksiyonu bekleyen bir yükleyiciyle kurulabilir. Aşağıdaki kod, log-analyzer deposundan alınmış bir yükleyici değil, yaklaşımı açıklayan bağımsız bir Python örneğidir; kimlik doğrulaması revizyonda eklenmiştir.

python
from importlib import import_module
from pkgutil import iter_modules


def discover(package_name: str = "plugins") -> dict[str, object]:
    package = import_module(package_name)
    prefix = f"{package.__name__}."
    registry: dict[str, object] = {}

    for module_info in iter_modules(package.__path__, prefix):
        module = import_module(module_info.name)
        factory = getattr(module, "create_plugin", None)

        if not callable(factory):
            raise RuntimeError(f"{module_info.name}: create_plugin() bulunamadı")

        plugin = factory()
        plugin_id = getattr(plugin, "plugin_id", "")
        if not isinstance(plugin_id, str) or not plugin_id:
            raise RuntimeError(f"{module_info.name}: geçerli plugin_id bulunamadı")
        if plugin_id in registry:
            raise RuntimeError(f"tekrarlanan plugin_id: {plugin_id}")
        registry[plugin_id] = plugin

    return registry

Aradaki fark yalnızca söz dizimi değildir. Go sürümünde derleyicinin yaptığı arayüz denetimi, Python sürümünde elle yazılan doğrulama koduna dönüşür; gerekli metotların varlığı, sürüm uyumu ve kimlik benzersizliği tek tek sınanmak zorundadır.

Uyarı

Her iki yaklaşımda da eklenti, ana uygulamayla aynı süreçte ve aynı yetkilerle çalışır. Derlenmiş bir eklenti de, çalışma anında yüklenmiş bir modül de uygulamanın dosya, ağ ve ortam değişkeni erişimine sahiptir. Güvenilmeyen eklentiler yalnızca hata yakalayarak güvenli hale gelmez; ayrı süreç, işletim sistemi izinleri, kaynak sınırları ve doğrulanmış dağıtım politikası gerekir.

Ayrı Süreçle Yalıtım ​

Projedeki netmetrics eklentisi, aynı süreç kuralının pratik bir istisnasını gösterir. Paket yakalama işi Go içinde değil, Scapy kullanan ayrı bir Python sürecinde yapılır. Betik yürütülebilir dosyaya gömülür, geçici dosyaya yazılır ve alt süreç olarak çalıştırılır.

go
//go:embed sniffer.py
var snifferPy []byte
go
ctx, stop := signal.NotifyContext(
	context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

proc := exec.CommandContext(ctx, python, pyArgs...)
proc.Stdout = os.Stdout
proc.Stderr = os.Stderr
proc.Stdin = os.Stdin

if err := proc.Run(); err != nil {
	if ctx.Err() != nil {
		return nil
	}
	return fmt.Errorf("scapy ajani basarisiz: %w", err)
}

Bu yapı, yakalama ajanını ayrı bir süreçte çalıştırarak bellek alanlarını ayırır. Python sürecindeki sıradan bir hata Go sürecini doğrudan çökertmez; ancak komut akışı proc.Run() sonucunu hata olarak aldığı için seçili netmetrics komutu yine başarısız olur. Ayrıca gösterilen exec.CommandContext çağrısı alt sürece kendiliğinden daha dar veya daha yüksek bir yetki vermez: alt süreç, varsayılan olarak ana sürecin kullanıcı ve yetkileriyle başlar. Yalnızca yakalama sürecine yüksek yetki vermek isteniyorsa ayrı bir ayrıcalık düşürme/yükseltme, capability, hizmet hesabı veya işletim sistemi sandbox yapılandırması gerekir. İki taraf arasındaki bağın komut satırı ve UDP mesaj biçimiyle açıkça tanımlanması ise süreçler arası sözleşmeyi görünür kılar.

Bedeli de görünürdür. Süreç başlatma maliyeti, Python ve Scapy bağımlılığı, iki tarafta ayrı tutulan sabitler. Betikteki eşik değerinin Go tarafındaki karşılığıyla aynı kalması gerektiği kod içinde not düşülmüştür; bu da süreçler arası sözleşmelerin tıpkı API'ler gibi belgelenmesi gerektiğini gösterir.

SOLID İlkeleri ile İlişki ​

SOLID beş tasarım ilkesinin kısaltmasıdır. Bu yazı, eklenti yapısıyla doğrudan ilişkili olan Open/Closed, Dependency Inversion, Single Responsibility ve Interface Segregation ilkelerine odaklanır.

SOLID'in beş ilkesi ve bu yazıda ele alınan Single Responsibility, Open/Closed, Interface Segregation ve Dependency Inversion ilkeleri.

SOLID beş ilkeden oluşur; bu yazıda S, O, I ve D ele alınır.

Open/Closed Principle ​

Ana uygulama, Plugin arayüzünü uygulayan yeni eklentileri kabul ederek genişler. Yeni bir yetenek için internal/cli/root.go dosyasına koşul veya somut eklenti import'u eklenmez; dizin açılır, sözleşme uygulanır ve kod üretimi çalıştırılır. Üretilen toplama dosyasının import listesi değişir, ancak bu değişiklik el ile yazılan uygulama çekirdeği iş mantığına yayılmaz. Bu yapı SOLID'in tamamını değil, özellikle Open/Closed ilkesini örnekler.

Değişime kapalı olmak, uygulama çekirdeğinin hiçbir zaman değişmeyeceği anlamına gelmez. Sözleşme değişirse, güvenlik düzeltmesi gerekirse veya yaşam döngüsü politikası değişirse uygulama çekirdeği de güncellenir. İlkenin amacı, olağan genişletmelerin mevcut davranışı sürekli değiştirmesini önlemektir. Buradaki “kapalı” kaynak kodun erişime kapalı olmasıyla ilgili değildir.

Open/Closed ilkesi: desteklenen API üzerinden genişleme ve olağan genişletmelerde mevcut iş mantığındaki değişiklik ihtiyacının azaltılması.

Chrome uzantıları desteklenen API'ler ve izinler çerçevesinde işlev ekler. Open/Closed'daki “kapalı”, kapalı kaynak lisansı anlamına gelmez; Chromium açık kaynaklı bir projedir. Chrome uzantı izinleri, Chromium projesi

Dependency Inversion Principle ​

Üst seviye CLI akışı LogAnalyzerPlugin veya NetMetricsPlugin gibi somut tiplere bağlı değildir. CLI bağlama kodu ve eklentiler ortak Plugin arayüzüne bağımlıdır; somut eklenti paketlerini yalnızca birleştirme noktası olan plugins paketi toplar. Bağımlılık okları kavramsal olarak hem üst seviye politika kodundan hem de somut eklentilerden soyut sözleşmeye yönelir. Bu, sadece araya bir API kutusu koymak değil, üst seviye kodun somut eklenti türlerine olan bağımlılığını tersine çevirmektir.

Somut eklentiye doğrudan bağımlılık ile ana uygulama ve eklentinin ortak sözleşmeye bağımlı olduğu yapının karşılaştırması.

Oklar kod bağımlılıklarını gösterir: ana uygulama → eklenti sözleşmesi ← eklenti. Chrome ve AdBlock adları kavramsal örnektir; şema gerçek tarayıcı iç mimarisinin bire bir gösterimi değildir. Bağımlılık ve kontrol akışı ayrımı

Dikkat edilmesi gereken nokta, sözleşmenin yalnızca metot imzalarından ibaret olmamasıdır. Komut adlarının çakışmaması, hata davranışı ve çıktı biçimi de sözleşmenin parçasıdır.

Single Responsibility Principle ​

Sorumluluklar ayrı dosyalara dağıtılmıştır:

  • internal/plugin/plugin.go sözleşmeyi tanımlar.
  • internal/plugin/register.go kayıt defterini ve kimlik doğrulamasını yönetir.
  • tools/gen keşif ve kod üretimini yapar.
  • internal/cli komutları bağlar ve çalıştırır.
  • Her eklenti kendi davranışını uygular.

Bu ayrım, bir eklentinin iş kuralı değiştiğinde kayıt defterinin; keşif yöntemi değiştiğinde ise eklentilerin değiştirilmesini önler.

Interface Segregation Principle ​

Plugin arayüzü üç metottan oluşur ve her eklenti üçünü de gerçekten kullanır. Arayüze ileride yalnızca bazı eklentilerin ihtiyaç duyacağı metotlar eklenmesi, geri kalan eklentileri boş gövdeli metot yazmaya zorlar. Böyle bir ihtiyaç doğduğunda doğru çözüm, ana arayüzü büyütmek yerine isteğe bağlı ikinci bir arayüz tanımlamak ve tip doğrulamasıyla denetlemektir.

Üretim Ortamında Dikkat Edilecek Konular ​

Sözleşme ve Sürümleme ​

Eklentiler ana uygulamayla birlikte derlendiği sürece kaynak düzeyindeki arayüz uyumsuzluklarının önemli bölümü derleme hatasına dönüşür. Mevcut sözleşme internal/plugin altında olduğu için eklentiler aynı modül ağacının dışından doğrudan bu paketi içe aktaramaz. Gerçekten ayrı depolarda geliştirilen veya ayrı dağıtılan eklentiler hedefleniyorsa sözleşme dışa açık bir modüle taşınmalı; sürüm bilgisi, uyumluluk politikası ve kayıt aşamasında açık hata iletisi eklenmelidir. Büyük sözleşme değişikliklerinde bir geçiş dönemi veya uyumluluk adaptörü gerekebilir.

Hata Yalıtımı ​

Bir eklentinin hatasının etkisi mümkün olduğunca sınırlandırılmalıdır. Komut tabanlı bu tasarımda bir çalıştırmada yalnızca seçilen eklentinin komutu yürütüldüğü için diğer eklentiler çoğunlukla devreye girmez; yine de aynı süreçteki panic, bellek bozulması veya kaynak tüketimi ana uygulamayı etkileyebilir. Eklentilerin aynı anda çağrıldığı bir modele geçilirse her çağrı açık hata, süre aşımı ve iptal politikalarıyla çevrelenmelidir. Süre aşımı, aşırı bellek kullanımı veya süreç çökmesi gibi sorunlar aynı süreçte tam olarak yalıtılamaz; daha güçlü sınırlar gerektiğinde ayrı süreç ve işletim sistemi kaynak kısıtları kullanılabilir.

Yükleme Sırası ve Bağımlılıklar ​

Go belirtimi paket başlatma sırasını bağımlılıklar ve paket yolu sırasına göre tanımlar. Yine de eklenti davranışını bu dolaylı sıraya bağlamak kırılgandır; uygulama düzeyindeki sıra açıkça modellenmelidir. All() fonksiyonunun ada göre sıralama yapması kullanıcıya görünen çıktıyı belirleyici hale getirir. Eklentiler birbirine bağımlı olursa ilişki açıkça tanımlanmalı, çevrimsel bağımlılıklar reddedilmeli ve ortak değiştirilebilir durum paylaşımından kaçınılmalıdır.

Yapılandırma ​

Her eklenti yalnızca kendi yapılandırma bölümüne erişmelidir. Ana uygulamanın tüm ayar nesnesini paylaşmak, eklentileri uygulama çekirdeğinin ayrıntılarına bağımlı hale getirir ve yetki sınırlarını genişletir.

Gözlemlenebilirlik ​

Kayıtlı eklenti adı ve sürümü, başlatma süresi, çalışma süresi, hata sayısı ve devre dışı bırakma nedeni kaydedilmelidir. Log ve metriklerde eklenti adının bulunması, hatanın kaynağını ayırmayı kolaylaştırır. Mevcut list komutu yalnızca ad ve açıklama sunduğu için temel bir envanter görünümüdür; sürüm ve sağlık bilgisini kapsayan tam bir gözlemlenebilirlik aracı değildir.

Test Edilebilirlik ​

Sözleşme açıkça tanımlandığında eklentiler ana uygulama çalıştırılmadan test edilebilir. Kayıt defteri tarafında boş ve tekrarlanan kimlikler ile sıralama; bağlama tarafında ise kimlik–komut adı eşleşmesi, yerleşik komut çakışması ve boş komut davranışı sınanmalıdır.

Güvenlik ​

Eklenti kodunun kaynağı, bütünlüğü ve yetkileri doğrulanmalıdır. İmzalı paketler, izin listeleri ve bağımlılık taraması tedarik zinciri riskini azaltabilir. Hassas işlemler, eklentiye geniş bir uygulama nesnesi vermek yerine dar yetkili servis arayüzleri üzerinden sunulmalıdır. Yüksek yetki gerektiren işler, ayrı süreçte ve yalnızca gereken izinlerle çalıştırılmalıdır.

Sonuç ​

Katmanlı tasarımın temel fikri sistemin her düzeyinde benzerdir: değişmesi beklenen kısmı görece kararlı bir arayüzün arkasına almak. Komut seti işlemci tasarımını, işletim sisteminin belgelenmiş kullanıcı alanı arayüzleri işletim sistemi çekirdeğinin ayrıntılarını, sürücü modeli donanım farklılıklarını, eklenti API'si ise uygulama çekirdeğini eklenti ayrıntılarından ayırır.

Eklenti sisteminin temeli bu nedenle kodu dizinlere bölmek değil, ana uygulama ile eklentiler arasında kararlı ve sınırlı bir sözleşme kurmaktır. log-analyzer örneğinde sözleşme üç metottan ibarettir; keşif derleme öncesindeki kod üretimiyle, kayıt init() fonksiyonlarıyla, bağlama ise kayıt defteri üzerinde dönen bir döngüyle yapılır. Uygulama çekirdeğinin genel CLI mantığı somut eklenti adlarını bilmez; bu adlar üretilen birleştirme dosyasında yer alır.

Open/Closed sistemin yeni eklentilerle genişletilmesini, Dependency Inversion ana uygulamanın somut tipler yerine sözleşmeye bağlanmasını, Single Responsibility keşif, kayıt ve eklenti davranışlarının ayrı kalmasını, Interface Segregation ise sözleşmenin dar tutulmasını destekler. Bu ilkeler, log analizi, ağ telemetrisi ve akış işleme gibi farklı yeteneklerin ortak bir uygulama çekirdeği çevresinde daha düşük bağlaşım ile geliştirilmesine yardımcı olur; aynı süreçte çalıştıkları ölçüde kusursuz hata yalıtımı sağladıkları anlamına gelmez.


Kaynakça ​

Eklenti (Plugin) Sistemi İçin Kaynakça