Sistem yöneticiliğinde en çok ihmal edilen konulardan biri log yönetimidir. Script yazıyorsun, çalışıyor, iş bitiyor ama bir şeyler ters gittiğinde geriye dönüp bakacak hiçbir kayıt yok. Ya da tam tersi, her şeyi logluyorsun ama dosyalar şişiyor, disk doluyor, önemli satırı bulmak için saatler harcıyorsun. Bu yazıda hem tarih damgalı, düzgün formatlı log tutan hem de çıktı yönetimini akıllıca yapan bash scriptleri nasıl yazılır, bunu gerçek dünya senaryolarıyla ele alacağız.
Neden Düzgün Log Tutmak Önemli?
Çoğu sysadmin script yazarken şöyle bir yaklaşım benimser: echo "İşlem tamamlandı" yazar ve geçer. Cron job’a bağlayınca da çıktı uçup gider. Bir hafta sonra “Bu script neden çalışmadı?” sorusuna cevap aramaya başlarsın ama elimde hiçbir şey yoktur.
Düzgün log yönetiminin sağladığı faydalar:
- Sorun tespiti: Hangi adımda hata oluştuğunu anında görebilirsin
- Zaman damgası: Ne zaman çalıştığını, ne kadar sürdüğünü takip edebilirsin
- Audit trail: Özellikle production ortamlarında kimin ne yaptığını kayıt altına alabilirsin
- Disk yönetimi: Log rotation ile dosyaların şişmesini engelleyebilirsin
- Monitoring entegrasyonu: Yapılandırılmış loglar Grafana, ELK gibi araçlara kolayca beslenebilir
Temel Log Fonksiyonu Yazmak
Her şeyin temeli iyi bir log fonksiyonu. Şöyle bir yapıdan başlayalım:
#!/bin/bash
# Log dosyası tanımı
LOG_FILE="/var/log/myscript/app.log"
LOG_DIR=$(dirname "$LOG_FILE")
# Log dizini yoksa oluştur
mkdir -p "$LOG_DIR"
# Temel log fonksiyonu
log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# Kullanım örnekleri
log "INFO" "Script başlatıldı"
log "WARN" "Disk kullanımı %80 üzerinde"
log "ERROR" "Veritabanı bağlantısı kurulamadı"
log "DEBUG" "Değişken değeri: $MY_VAR"
Bu yapıda tee -a kullanmak önemli. Hem ekrana hem de dosyaya yazıyor. Cron job’larda ekrana yazmak gereksiz olabilir ama geliştirme aşamasında bunu görmek hayat kurtarır.
Renk Destekli Terminal Çıktısı
Log seviyeleri renkli gösterilirse okunması çok daha kolay olur. Bunu terminal çıktısına ekleyebiliriz ama log dosyasına ANSI kodları yazmamalıyız:
#!/bin/bash
LOG_FILE="/var/log/myscript/app.log"
mkdir -p "$(dirname "$LOG_FILE")"
# Renk kodları
RED='33[0;31m'
YELLOW='33[1;33m'
GREEN='33[0;32m'
BLUE='33[0;34m'
NC='33[0m' # No Color
log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Dosyaya temiz yaz (renk kodu olmadan)
echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
# Terminale renkli yaz
case "$level" in
"INFO") echo -e "${GREEN}[$timestamp] [INFO]${NC} $message" ;;
"WARN") echo -e "${YELLOW}[$timestamp] [WARN]${NC} $message" ;;
"ERROR") echo -e "${RED}[$timestamp] [ERROR]${NC} $message" ;;
"DEBUG") echo -e "${BLUE}[$timestamp] [DEBUG]${NC} $message" ;;
*) echo "[$timestamp] [$level] $message" ;;
esac
}
Tarih Bazlı Log Dosyaları
Tek bir dosyaya sürekli yazmak yerine günlük, haftalık veya aylık log dosyaları oluşturmak yönetimi kolaylaştırır. Özellikle büyük sistemlerde bu yaklaşım şart:
#!/bin/bash
SCRIPT_NAME="backup_script"
LOG_BASE_DIR="/var/log/$SCRIPT_NAME"
# Tarih bazlı log dosyası
get_log_file() {
local date_str
date_str=$(date '+%Y-%m-%d')
local log_dir="$LOG_BASE_DIR/$date_str"
mkdir -p "$log_dir"
echo "$log_dir/${SCRIPT_NAME}.log"
}
# Her çağrıda günün log dosyasını kullan
LOG_FILE=$(get_log_file)
log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$$] [$level] $message" >> "$LOG_FILE"
echo "[$timestamp] [$level] $message"
}
log "INFO" "Script PID $$ ile başlatıldı"
Burada $$ ile process ID’yi de loga ekliyoruz. Aynı anda birden fazla instance çalışıyorsa hangi satırın hangi process’e ait olduğunu bu sayede ayırt edebilirsin.
Stdout ve Stderr Yönetimi
Bash’te çıktı yönetiminin en kritik parçası stdout ve stderr’i doğru yönlendirmek. Bir çok script bunu yanlış yapıyor:
#!/bin/bash
LOG_FILE="/var/log/myscript/output.log"
ERROR_LOG="/var/log/myscript/error.log"
mkdir -p "$(dirname "$LOG_FILE")"
# Tüm çıktıyı log dosyasına yönlendir
# Hem stdout hem stderr'i ayrı dosyalara yaz
exec 1>>"$LOG_FILE" # stdout'u log dosyasına bağla
exec 2>>"$ERROR_LOG" # stderr'i error log'a bağla
echo "Bu satır LOG_FILE'a gider"
ls /olmayan/dizin # Bu hata ERROR_LOG'a gider
# Alternatif: İkisini de aynı dosyaya yaz
# exec > >(tee -a "$LOG_FILE") 2>&1
Pipe ile Çalışan Komutların Loglanması
Bazen bir komutun çıktısını işlerken hem loglayıp hem de bir sonraki aşamaya geçirmen gerekir. tee bu iş için biçilmiş kaftandır:
#!/bin/bash
LOG_FILE="/var/log/myscript/pipeline.log"
mkdir -p "$(dirname "$LOG_FILE")"
timestamp() {
date '+%Y-%m-%d %H:%M:%S'
}
# Pipe içinde loglama
# Her satıra timestamp ekleyerek logla
find /var/www -name "*.php" |
tee >(while IFS= read -r line; do
echo "[$(timestamp)] FOUND: $line" >> "$LOG_FILE"
done) |
wc -l |
xargs -I{} echo "Toplam {} PHP dosyası bulundu"
echo "[$(timestamp)] Tarama tamamlandı" >> "$LOG_FILE"
Gerçek Dünya Senaryosu: Backup Script’i
Şimdi bunların hepsini bir araya getirelim. Bir yedekleme scripti yazalım, tam anlamıyla production’a alınabilir düzeyde:
#!/bin/bash
# =============================================================
# backup_mysql.sh - MySQL Yedekleme ve Log Yönetimi
# =============================================================
set -euo pipefail
# Konfigürasyon
SCRIPT_NAME="backup_mysql"
BACKUP_DIR="/backups/mysql"
LOG_BASE="/var/log/$SCRIPT_NAME"
DB_HOST="localhost"
DB_USER="backup_user"
DB_PASS="${DB_PASSWORD:-}" # Ortam değişkeninden al
RETENTION_DAYS=30
# Tarih damgaları
DATE=$(date '+%Y-%m-%d')
DATETIME=$(date '+%Y-%m-%d_%H-%M-%S')
LOG_FILE="$LOG_BASE/${DATE}.log"
BACKUP_PATH="$BACKUP_DIR/$DATE"
# Dizinleri oluştur
mkdir -p "$LOG_BASE" "$BACKUP_PATH"
# Log fonksiyonu
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [PID:$$] [$level] $message" | tee -a "$LOG_FILE"
}
# Hata yakalama
trap 'log "ERROR" "Script başarısız oldu. Satır: $LINENO"' ERR
trap 'log "INFO" "Script sonlandı. Süre: $((SECONDS))s"' EXIT
log "INFO" "=========================================="
log "INFO" "Yedekleme başlatıldı"
log "INFO" "Hedef dizin: $BACKUP_PATH"
# Veritabanı listesini al
log "INFO" "Veritabanı listesi alınıyor..."
DB_LIST=$(mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS"
-e "SHOW DATABASES;" 2>>"$LOG_FILE" |
grep -Ev "(Database|information_schema|performance_schema|sys)")
DB_COUNT=$(echo "$DB_LIST" | wc -l)
log "INFO" "Toplam $DB_COUNT veritabanı bulundu"
# Her veritabanını yedekle
SUCCESS=0
FAILED=0
while IFS= read -r db; do
[ -z "$db" ] && continue
DUMP_FILE="$BACKUP_PATH/${db}_${DATETIME}.sql.gz"
log "INFO" "Yedekleniyor: $db"
if mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS"
--single-transaction --quick "$db" 2>>"$LOG_FILE" |
gzip > "$DUMP_FILE"; then
SIZE=$(du -sh "$DUMP_FILE" | cut -f1)
log "INFO" "Başarılı: $db ($SIZE)"
((SUCCESS++))
else
log "ERROR" "Başarısız: $db"
((FAILED++))
fi
done <<< "$DB_LIST"
log "INFO" "Yedekleme özeti: $SUCCESS başarılı, $FAILED başarısız"
# Eski yedekleri temizle
log "INFO" "Eski yedekler temizleniyor ($RETENTION_DAYS günden eski)..."
DELETED=$(find "$BACKUP_DIR" -type d -mtime +$RETENTION_DAYS -exec rm -rf {} + 2>/dev/null; echo $?)
log "INFO" "Temizlik tamamlandı"
# Özet raporu
BACKUP_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1)
log "INFO" "Toplam yedek boyutu: $BACKUP_SIZE"
Log Rotation Entegrasyonu
Log dosyaları zamanla şişer. logrotate ile bunu otomatize edebilirsin. Script’in yanına bir de logrotate konfigürasyonu hazırlayalım:
#!/bin/bash
# setup_logrotate.sh - Log rotation konfigürasyonu kur
LOGROTATE_CONF="/etc/logrotate.d/myscripts"
cat > "$LOGROTATE_CONF" << 'EOF'
/var/log/backup_mysql/*.log
/var/log/myscript/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 root adm
dateext
dateformat -%Y-%m-%d
sharedscripts
postrotate
# Gerekirse servisi reload et
# systemctl reload myservice > /dev/null 2>&1 || true
echo "Log rotation tamamlandı: $(date)" >> /var/log/rotation.log
endscript
}
EOF
echo "Logrotate konfigürasyonu oluşturuldu: $LOGROTATE_CONF"
logrotate -d "$LOGROTATE_CONF" # Test modunda çalıştır
Log Analizi için Yardımcı Fonksiyonlar
Log tutmak kadar önemli olan şey, loglara kolayca bakabilmek. İşte hızlı analiz için birkaç fonksiyon:
#!/bin/bash
# log_analyzer.sh - Log analiz araçları
LOG_DIR="/var/log/myscript"
# Belirli seviyedeki logları filtrele
filter_logs() {
local level="$1"
local log_file="${2:-}"
if [ -n "$log_file" ]; then
grep "[$level]" "$log_file"
else
grep -r "[$level]" "$LOG_DIR/" 2>/dev/null
fi
}
# Son N satırı izle (canlı)
watch_logs() {
local lines="${1:-50}"
local log_file
# En son değiştirilen log dosyasını bul
log_file=$(find "$LOG_DIR" -name "*.log" -type f |
xargs ls -t 2>/dev/null | head -1)
if [ -z "$log_file" ]; then
echo "Log dosyası bulunamadı: $LOG_DIR"
exit 1
fi
echo "İzleniyor: $log_file"
tail -n "$lines" -f "$log_file"
}
# Hata özeti çıkar
error_summary() {
local days="${1:-7}"
echo "Son $days gündeki hata özeti:"
echo "================================"
find "$LOG_DIR" -name "*.log" -mtime -"$days" |
xargs grep -h "[ERROR]" 2>/dev/null |
awk '{print $NF}' |
sort | uniq -c | sort -rn | head -20
}
# İstatistik raporu
log_stats() {
local log_file="${1:-}"
[ -z "$log_file" ] && log_file=$(find "$LOG_DIR" -name "*.log" | sort | tail -1)
echo "Log Dosyası: $log_file"
echo "Toplam satır: $(wc -l < "$log_file")"
echo "INFO sayısı: $(grep -c '[INFO]' "$log_file" || echo 0)"
echo "WARN sayısı: $(grep -c '[WARN]' "$log_file" || echo 0)"
echo "ERROR sayısı: $(grep -c '[ERROR]' "$log_file" || echo 0)"
echo "DEBUG sayısı: $(grep -c '[DEBUG]' "$log_file" || echo 0)"
echo "İlk kayıt: $(head -1 "$log_file" | awk '{print $1, $2}' | tr -d '[]')"
echo "Son kayıt: $(tail -1 "$log_file" | awk '{print $1, $2}' | tr -d '[]')"
}
# Argümana göre çalıştır
case "${1:-help}" in
errors) error_summary "${2:-7}" ;;
watch) watch_logs "${2:-50}" ;;
stats) log_stats "${2:-}" ;;
filter) filter_logs "${2:-ERROR}" "${3:-}" ;;
*)
echo "Kullanım: $0 {errors|watch|stats|filter} [parametreler]"
echo " errors [gün] - Son N günün hata özeti (varsayılan: 7)"
echo " watch [satır] - Canlı log izleme (varsayılan: 50)"
echo " stats [dosya] - Log istatistikleri"
echo " filter [seviye] [dosya] - Seviyeye göre filtrele"
;;
esac
Yapılandırılmış Log Formatı: JSON
Eğer logları bir SIEM sistemine veya Elasticsearch’e besleyeceksen JSON format çok daha uygun olur:
#!/bin/bash
# json_logger.sh - Yapılandırılmış JSON loglama
LOG_FILE="/var/log/myscript/structured.log"
mkdir -p "$(dirname "$LOG_FILE")"
HOSTNAME=$(hostname -s)
SCRIPT_NAME=$(basename "$0" .sh)
json_log() {
local level="$1"
local message="$2"
local extra="${3:-}"
local timestamp
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ') # ISO 8601 UTC
# Basit JSON oluştur (jq olmadan)
local json
json=$(printf '{"timestamp":"%s","level":"%s","host":"%s","script":"%s","pid":%d,"message":"%s"'
"$timestamp" "$level" "$HOSTNAME" "$SCRIPT_NAME" "$$"
"$(echo "$message" | sed 's/"/\"/g')")
# Ek alan varsa ekle
if [ -n "$extra" ]; then
json="${json},${extra}"
fi
json="${json}}"
echo "$json" >> "$LOG_FILE"
# Terminale okunabilir format
echo "[$timestamp] [$level] $message"
}
# Kullanım örnekleri
json_log "INFO" "Sistem yedeklemesi başladı" '"backup_type":"full","db_count":5'
json_log "ERROR" "Bağlantı zaman aşımı" '"host":"db01","timeout_seconds":30'
json_log "INFO" "Yedekleme tamamlandı" '"duration_seconds":142,"size_mb":1024'
Bu format Filebeat veya Fluentd ile doğrudan toplanabilir. Logların merkezi bir yerde toplanması gereken büyük ortamlarda bu yaklaşım çok değer kazanır.
Script Genelinde Loglama: Kütüphane Yaklaşımı
Birden fazla script yazıyorsan, log fonksiyonlarını tekrar tekrar yazmak yerine bir kütüphane dosyası oluştur ve source et:
#!/bin/bash
# /usr/local/lib/bash/logging.sh
# Kullanım: source /usr/local/lib/bash/logging.sh
# Dışarıdan override edilebilir değişkenler
LOG_LEVEL="${LOG_LEVEL:-INFO}" # DEBUG, INFO, WARN, ERROR
LOG_FILE="${LOG_FILE:-/tmp/script.log}"
LOG_TO_STDOUT="${LOG_TO_STDOUT:-true}"
LOG_MAX_SIZE="${LOG_MAX_SIZE:-10485760}" # 10MB
# Seviye sıralaması
declare -A LOG_LEVELS=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3)
_should_log() {
local msg_level="$1"
local current_level_num="${LOG_LEVELS[$LOG_LEVEL]:-1}"
local msg_level_num="${LOG_LEVELS[$msg_level]:-1}"
[ "$msg_level_num" -ge "$current_level_num" ]
}
_rotate_if_needed() {
if [ -f "$LOG_FILE" ]; then
local size
size=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$size" -gt "$LOG_MAX_SIZE" ]; then
mv "$LOG_FILE" "${LOG_FILE}.$(date '+%Y%m%d%H%M%S').old"
fi
fi
}
_write_log() {
local level="$1"
local message="$2"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local caller_info=""
# Çağıran fonksiyon ve satır numarasını ekle (DEBUG modunda)
if [ "$LOG_LEVEL" = "DEBUG" ]; then
caller_info=" [${BASH_SOURCE[2]##*/}:${BASH_LINENO[1]}]"
fi
local log_line="[$timestamp] [$level]${caller_info} $message"
mkdir -p "$(dirname "$LOG_FILE")"
_rotate_if_needed
echo "$log_line" >> "$LOG_FILE"
if [ "$LOG_TO_STDOUT" = "true" ]; then
echo "$log_line"
fi
}
# Public fonksiyonlar
log_debug() { _should_log "DEBUG" && _write_log "DEBUG" "$*"; return 0; }
log_info() { _should_log "INFO" && _write_log "INFO" "$*"; return 0; }
log_warn() { _should_log "WARN" && _write_log "WARN" "$*"; return 0; }
log_error() { _should_log "ERROR" && _write_log "ERROR" "$*"; return 0; }
log_info "Logging kütüphanesi yüklendi (seviye: $LOG_LEVEL)"
Bu kütüphaneyi kullanan bir script:
#!/bin/bash
# my_deploy_script.sh
export LOG_FILE="/var/log/deploy/$(date '+%Y-%m-%d')_deploy.log"
export LOG_LEVEL="DEBUG" # Geliştirme ortamı için
export LOG_TO_STDOUT="true"
source /usr/local/lib/bash/logging.sh
log_info "Deploy başlıyor: $APP_NAME v${VERSION:-unknown}"
log_debug "Ortam değişkenleri kontrol ediliyor"
if [ -z "${APP_NAME:-}" ]; then
log_error "APP_NAME tanımlanmamış!"
exit 1
fi
log_warn "Servis yeniden başlatılacak, kısa bir kesinti yaşanabilir"
# ... deploy işlemleri ...
log_info "Deploy tamamlandı"
Pratik İpuçları ve Dikkat Edilmesi Gerekenler
Yıllarca script yazıp bakımını yaparken edindiğim bazı alışkanlıklar var:
set -euo pipefailkullan: Script hata alınca dur, tanımsız değişken kullanımını engelle, pipe hatalarını yakala- Hassas bilgileri loglama: Parola, token, API key gibi bilgiler log dosyasına asla yazılmamalı
- Log dosyası izinlerine dikkat et:
chmod 640ve doğru ownership ile hassas sistem bilgilerini koruyun - Disk doluluk kontrolü ekle: Log yazmadan önce disk alanı kontrol et, özellikle uzun süren işlemlerde
- Script başlangıç ve bitiş sürelerini logla:
$SECONDSdeğişkeni bash’te saniye bazında süre verir - PID ve hostname ekle: Dağıtık ortamlarda hangi makineden geldiğini anlamak için şart
- Cron job’lardan e-posta bildirimi: Log tutmak yetmez, kritik hatalar için
/usr/bin/mailveyasendmailentegrasyonu da düşün - Geçici dosyaları temizle: Script sonlandığında geçici dosyalar
trapile silinmeli
Sonuç
Log tutmak bir külfet değil, bir yatırım. Script’ini bir kez düzgün log altyapısıyla yazarsın, sonrasında hata ayıklama için harcadığın saat sayısı dramatik şekilde düşer. Bu yazıda anlattığımız yaklaşımları özetlemek gerekirse:
Basit bir log fonksiyonuyla başla, her satıra zaman damgası ve seviye ekle. stdout ile stderr‘i ayrı tut ya da ikisini de aynı dosyaya yönlendir ama bilinçli bir karar ver. Tarih bazlı dosya isimlendirmesiyle logları organize et, logrotate ile şişmesini engelle. Birden fazla script yazıyorsan merkezi bir logging kütüphanesi oluştur ve source et. Büyük altyapılarda JSON formatına geç.
En iyi script, çalışmadığında bile sana neyin yanlış gittiğini söyleyen script’tir.