API Yanıtlarını İşleme: JSON Parse ve Hata Yönetimi
Modern sistemlerde neredeyse her şey bir API üzerinden konuşuyor. Monitoring sistemlerin veri çektiği servisler, deployment pipeline’larının tetiklediği webhook’lar, kullanıcı kimlik doğrulama akışları… Hepsinin ortak noktası JSON formatında veri alışverişi yapması. Peki bu verileri bash script’lerinde veya otomasyon araçlarında düzgün işleyemizsek ne olur? En iyi ihtimalle yanlış bir değer okursun, en kötü ihtimalle production sisteminde sessiz sedasız bir hata oluşur ve saatler sonra fark edersin. Bu yazıda API yanıtlarını nasıl doğru parse edeceğimizi, hataları nasıl yöneteceğimizi ve gerçek dünya senaryolarında nelere dikkat etmemiz gerektiğini ele alacağız.
Temel Araçlar: jq ile Tanışma
Linux dünyasında JSON işlemenin standart aracı jq‘dur. Hafif, hızlı ve son derece güçlü bir komut satırı aracı. Eğer henüz kurulu değilse:
# Debian/Ubuntu
apt-get install jq -y
# RHEL/CentOS
yum install jq -y
# macOS
brew install jq
jq‘nun temel mantığını anlamak için önce basit bir örnekle başlayalım. Bir API’den kullanıcı bilgisi çektiğimizi varsayalım:
#!/bin/bash
# Basit JSON parse örneği
API_RESPONSE='{
"status": "success",
"user": {
"id": 1234,
"name": "Ahmet Yilmaz",
"email": "[email protected]",
"roles": ["admin", "developer"]
}
}'
# Tek bir alan okuma
USERNAME=$(echo "$API_RESPONSE" | jq -r '.user.name')
echo "Kullanici: $USERNAME"
# Array elemanına erişim
FIRST_ROLE=$(echo "$API_RESPONSE" | jq -r '.user.roles[0]')
echo "Birincil rol: $FIRST_ROLE"
# Tüm rolleri listeleme
echo "Tüm roller:"
echo "$API_RESPONSE" | jq -r '.user.roles[]'
Burada -r parametresi raw output anlamına geliyor. Bu parametre olmadan jq, string değerleri tırnak işaretleriyle döndürür. Bash script’lerinde genellikle -r kullanmak isteyeceksiniz.
Gerçek API Çağrıları ve curl Entegrasyonu
Teoriden pratiğe geçelim. Gerçek bir API çağrısını curl ile yapıp yanıtı işleyelim:
#!/bin/bash
# GitHub API'den repository bilgisi çekme
GITHUB_TOKEN="ghp_xxxxxxxxxxxx"
REPO_OWNER="mycompany"
REPO_NAME="myproject"
# API çağrısını yap ve yanıtı değişkende sakla
API_RESPONSE=$(curl -s
-H "Authorization: token $GITHUB_TOKEN"
-H "Accept: application/vnd.github.v3+json"
"https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}")
# HTTP status code'u ayrı al
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}"
-H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}")
echo "HTTP Status: $HTTP_STATUS"
# Yanıttan bilgi çıkar
REPO_DESC=$(echo "$API_RESPONSE" | jq -r '.description')
STAR_COUNT=$(echo "$API_RESPONSE" | jq -r '.stargazers_count')
DEFAULT_BRANCH=$(echo "$API_RESPONSE" | jq -r '.default_branch')
echo "Açıklama: $REPO_DESC"
echo "Yıldız: $STAR_COUNT"
echo "Ana branch: $DEFAULT_BRANCH"
Yukarıdaki örnekte iki ayrı curl çağrısı var, bu verimsiz. Daha iyi bir yöntem:
#!/bin/bash
# Tek curl çağrısıyla hem body hem status code alma
TEMP_FILE=$(mktemp)
HTTP_STATUS=$(curl -s
-H "Authorization: token $GITHUB_TOKEN"
-H "Accept: application/vnd.github.v3+json"
-o "$TEMP_FILE"
-w "%{http_code}"
"https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}")
API_RESPONSE=$(cat "$TEMP_FILE")
rm -f "$TEMP_FILE"
echo "HTTP Status: $HTTP_STATUS"
echo "Response body mevcut: $(echo "$API_RESPONSE" | wc -c) byte"
Bu yaklaşım daha temiz. Tek bir network isteğiyle hem body’yi hem status code’u alıyoruz.
HTTP Hata Kodlarını Yönetme
Sysadmin olarak en çok başımıza gelen sorun şu: script çalışıyor gibi görünüyor ama aslında API hata döndürüyor ve biz bunu fark etmiyoruz. Kapsamlı bir hata yönetimi fonksiyonu yazalım:
#!/bin/bash
# Kapsamlı API hata yönetimi
API_BASE_URL="https://api.example.com"
API_KEY="your-api-key-here"
LOG_FILE="/var/log/api_client.log"
log() {
local level=$1
shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a "$LOG_FILE"
}
make_api_request() {
local endpoint=$1
local method=${2:-GET}
local body=${3:-}
local temp_file
temp_file=$(mktemp)
local curl_opts=(
-s
-X "$method"
-H "Authorization: Bearer $API_KEY"
-H "Content-Type: application/json"
-H "Accept: application/json"
-o "$temp_file"
-w "%{http_code}"
--connect-timeout 10
--max-time 30
)
if [[ -n "$body" ]]; then
curl_opts+=(-d "$body")
fi
local http_status
http_status=$(curl "${curl_opts[@]}" "${API_BASE_URL}${endpoint}")
local curl_exit_code=$?
local response_body
response_body=$(cat "$temp_file")
rm -f "$temp_file"
# curl'ün kendisinde hata var mı?
if [[ $curl_exit_code -ne 0 ]]; then
log "ERROR" "curl başarısız oldu. Exit code: $curl_exit_code, Endpoint: $endpoint"
case $curl_exit_code in
6) log "ERROR" "DNS çözümlenemedi: $API_BASE_URL" ;;
7) log "ERROR" "Bağlantı reddedildi" ;;
28) log "ERROR" "Timeout oluştu (30s aşıldı)" ;;
*) log "ERROR" "Bilinmeyen curl hatası" ;;
esac
return 1
fi
# HTTP status code kontrolü
case $http_status in
200|201|202|204)
log "INFO" "Başarılı: $method $endpoint ($http_status)"
echo "$response_body"
return 0
;;
400)
log "ERROR" "Bad Request (400): Gönderilen veri hatalı"
local error_msg
error_msg=$(echo "$response_body" | jq -r '.message // .error // "Bilinmeyen hata"')
log "ERROR" "Sunucu mesajı: $error_msg"
return 2
;;
401)
log "ERROR" "Unauthorized (401): API key geçersiz veya süresi dolmuş"
return 3
;;
403)
log "ERROR" "Forbidden (403): Bu işlem için yetkiniz yok"
return 4
;;
404)
log "ERROR" "Not Found (404): $endpoint bulunamadı"
return 5
;;
429)
log "WARN" "Rate Limit (429): Çok fazla istek gönderildi"
local retry_after
retry_after=$(echo "$response_body" | jq -r '.retry_after // 60')
log "WARN" "${retry_after} saniye bekleyip tekrar deneyin"
return 6
;;
5*)
log "ERROR" "Sunucu hatası ($http_status): API tarafında sorun var"
return 7
;;
*)
log "ERROR" "Beklenmedik HTTP status: $http_status"
return 8
;;
esac
}
Bu fonksiyonu gerçek bir senaryoda şöyle kullanırsın:
# Kullanım örneği
response=$(make_api_request "/v1/users/1234")
exit_code=$?
if [[ $exit_code -eq 0 ]]; then
user_name=$(echo "$response" | jq -r '.name')
user_email=$(echo "$response" | jq -r '.email')
log "INFO" "Kullanıcı bulundu: $user_name ($user_email)"
else
log "ERROR" "Kullanıcı bilgisi alınamadı, exit code: $exit_code"
exit 1
fi
JSON Doğrulama ve Null Değer Kontrolü
API yanıtı bazen beklediğimiz alanı içermeyebilir. Bu durumu handle etmeden jq çıktısını kullanan kod ciddi sorunlara yol açabilir:
#!/bin/bash
# JSON doğrulama ve null kontrolü
validate_json_response() {
local response=$1
# Önce geçerli JSON mı kontrol et
if ! echo "$response" | jq -e . > /dev/null 2>&1; then
echo "HATA: Geçersiz JSON yanıtı alındı"
echo "Ham yanıt: $response"
return 1
fi
return 0
}
safe_jq_extract() {
local json=$1
local path=$2
local default_value=${3:-""}
local value
value=$(echo "$json" | jq -r "$path // empty")
if [[ -z "$value" ]]; then
echo "$default_value"
else
echo "$value"
fi
}
# Örnek kullanım
API_RESPONSE='{"user": {"name": "Mehmet", "age": null, "department": ""}}'
validate_json_response "$API_RESPONSE" || exit 1
USERNAME=$(safe_jq_extract "$API_RESPONSE" '.user.name' "Bilinmeyen")
USER_AGE=$(safe_jq_extract "$API_RESPONSE" '.user.age' "0")
DEPARTMENT=$(safe_jq_extract "$API_RESPONSE" '.user.department' "Atanmamış")
echo "İsim: $USERNAME"
echo "Yaş: $USER_AGE"
echo "Departman: $DEPARTMENT"
// empty sözdizimi önemli. jq‘da // operatörü “alternatif” operatörüdür. Değer null veya false ise sağ taraftaki değeri döndürür. empty ise hiçbir çıktı üretmez, böylece bash tarafında boş string olarak yakalanır.
Rate Limiting ve Retry Mekanizması
Production ortamında API’lere istek atarken rate limiting meselesi kaçınılmaz olarak karşınıza çıkar. Exponential backoff stratejisi ile retry mekanizması kurmalısınız:
#!/bin/bash
# Retry ve exponential backoff implementasyonu
MAX_RETRIES=5
INITIAL_WAIT=1
MAX_WAIT=60
api_request_with_retry() {
local endpoint=$1
local method=${2:-GET}
local body=${3:-}
local retry_count=0
local wait_time=$INITIAL_WAIT
while [[ $retry_count -le $MAX_RETRIES ]]; do
if [[ $retry_count -gt 0 ]]; then
echo "Deneme $retry_count/$MAX_RETRIES - ${wait_time}s bekleniyor..."
sleep "$wait_time"
# Exponential backoff: bekleme süresini ikiye katla
wait_time=$((wait_time * 2))
# Maksimum bekleme süresini aşma
[[ $wait_time -gt $MAX_WAIT ]] && wait_time=$MAX_WAIT
fi
local temp_file
temp_file=$(mktemp)
local http_status
http_status=$(curl -s
-X "$method"
-H "Authorization: Bearer $API_KEY"
-H "Content-Type: application/json"
-o "$temp_file"
-w "%{http_code}"
--connect-timeout 10
--max-time 30
"${body:+-d $body}"
"${API_BASE_URL}${endpoint}")
local curl_exit=$?
local response
response=$(cat "$temp_file")
rm -f "$temp_file"
# Başarılı yanıt
if [[ $http_status -ge 200 && $http_status -lt 300 ]]; then
echo "$response"
return 0
fi
# Rate limit - Retry-After header'ına göre bekle
if [[ $http_status -eq 429 ]]; then
local retry_after
retry_after=$(echo "$response" | jq -r '.retry_after // 0')
if [[ $retry_after -gt 0 ]]; then
echo "Rate limit aşıldı. Sunucu $retry_after saniye beklemesini istiyor."
wait_time=$retry_after
fi
fi
# 4xx hatalar genellikle retry etmeye değmez (401, 403, 404)
if [[ $http_status -ge 400 && $http_status -lt 500 && $http_status -ne 429 ]]; then
echo "HATA: $http_status - Retry yapılmayacak"
echo "$response"
return 1
fi
# 5xx ve network hataları için retry
retry_count=$((retry_count + 1))
echo "UYARI: HTTP $http_status alındı, tekrar deneniyor ($retry_count/$MAX_RETRIES)"
done
echo "HATA: Maksimum retry sayısına ulaşıldı ($MAX_RETRIES)"
return 1
}
Gerçek Dünya Senaryosu: Monitoring Entegrasyonu
Şimdi her şeyi bir araya getirelim. Birçok şirkette Alertmanager veya PagerDuty gibi sistemlerle entegrasyon yapmak gerekiyor. Aşağıdaki script, bir monitoring API’sinden kritik uyarıları çekip Slack’e bildirim gönderiyor:
#!/bin/bash
# Prometheus Alertmanager API entegrasyonu ve Slack bildirimi
ALERTMANAGER_URL="http://alertmanager.internal:9093"
SLACK_WEBHOOK_URL="https://hooks.slack.com/services/xxx/yyy/zzz"
LOG_FILE="/var/log/alert_notifier.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
# Aktif alertleri çek
fetch_active_alerts() {
local response
local http_status
local temp_file
temp_file=$(mktemp)
http_status=$(curl -s
-o "$temp_file"
-w "%{http_code}"
--connect-timeout 5
--max-time 15
"${ALERTMANAGER_URL}/api/v2/alerts?active=true&silenced=false")
response=$(cat "$temp_file")
rm -f "$temp_file"
if [[ $http_status -ne 200 ]]; then
log "ERROR: Alertmanager erişilemedi. HTTP: $http_status"
return 1
fi
# JSON doğrulama
if ! echo "$response" | jq -e . > /dev/null 2>&1; then
log "ERROR: Geçersiz JSON yanıtı"
return 1
fi
echo "$response"
}
# Kritik alertları filtrele ve Slack mesajı oluştur
process_alerts() {
local alerts_json=$1
# Kritik severity'deki alertları say
local critical_count
critical_count=$(echo "$alerts_json" | jq '[.[] | select(.labels.severity == "critical")] | length')
if [[ $critical_count -eq 0 ]]; then
log "INFO: Kritik alert yok"
return 0
fi
log "WARN: $critical_count adet kritik alert tespit edildi"
# Her kritik alert için mesaj oluştur
local slack_text="*:red_circle: $critical_count Kritik Alert Tespit Edildi*n"
while IFS= read -r alert; do
local alert_name
local instance
local description
local started_at
alert_name=$(echo "$alert" | jq -r '.labels.alertname // "Bilinmeyen"')
instance=$(echo "$alert" | jq -r '.labels.instance // "N/A"')
description=$(echo "$alert" | jq -r '.annotations.description // .annotations.summary // "Açıklama yok"')
started_at=$(echo "$alert" | jq -r '.startsAt')
slack_text+="• *$alert_name* - Instance: `$instance`n"
slack_text+=" $descriptionn"
slack_text+=" Başlangıç: $started_atnn"
log "CRITICAL ALERT: $alert_name on $instance"
done < <(echo "$alerts_json" | jq -c '.[] | select(.labels.severity == "critical")')
# Slack'e gönder
local slack_payload
slack_payload=$(jq -n --arg text "$slack_text" '{
"text": $text,
"mrkdwn": true
}')
local slack_response
local slack_status
local temp_file
temp_file=$(mktemp)
slack_status=$(curl -s
-X POST
-H "Content-Type: application/json"
-d "$slack_payload"
-o "$temp_file"
-w "%{http_code}"
"$SLACK_WEBHOOK_URL")
slack_response=$(cat "$temp_file")
rm -f "$temp_file"
if [[ $slack_status -eq 200 ]]; then
log "INFO: Slack bildirimi gönderildi"
else
log "ERROR: Slack bildirimi gönderilemedi. HTTP: $slack_status, Response: $slack_response"
fi
}
# Ana akış
main() {
log "INFO: Alert kontrolü başlatılıyor"
local alerts
if ! alerts=$(fetch_active_alerts); then
log "ERROR: Alert verisi alınamadı, çıkılıyor"
exit 1
fi
process_alerts "$alerts"
log "INFO: Alert kontrolü tamamlandı"
}
main "$@"
JSON Array’lerini İşleme ve Döngüler
Çok kayıtlı API yanıtlarını işlemek başlı başına bir konu. Sayfalama (pagination) desteği olan bir API’den tüm kayıtları çeken bir örnek:
#!/bin/bash
# Paginated API response işleme
API_URL="https://api.example.com"
API_KEY="your-key"
fetch_all_users() {
local page=1
local per_page=100
local all_users="[]"
local has_more=true
while [[ "$has_more" == "true" ]]; do
echo "Sayfa $page çekiliyor..."
local response
local temp_file
temp_file=$(mktemp)
local http_status
http_status=$(curl -s
-H "Authorization: Bearer $API_KEY"
-o "$temp_file"
-w "%{http_code}"
"${API_URL}/v1/users?page=${page}&per_page=${per_page}")
response=$(cat "$temp_file")
rm -f "$temp_file"
if [[ $http_status -ne 200 ]]; then
echo "HATA: HTTP $http_status alındı"
return 1
fi
# Bu sayfadaki kayıt sayısını kontrol et
local page_count
page_count=$(echo "$response" | jq '.data | length')
if [[ $page_count -eq 0 ]]; then
has_more=false
echo "Tüm sayfalar işlendi"
break
fi
# Bu sayfanın kullanıcılarını genel listeye ekle
all_users=$(echo "$all_users $response" | jq -s '.[0] + .[1].data')
# Sonraki sayfa var mı?
local next_page
next_page=$(echo "$response" | jq -r '.meta.next_page // empty')
if [[ -z "$next_page" ]]; then
has_more=false
else
page=$((page + 1))
fi
done
echo "$all_users"
}
# Tüm kullanıcıları çek ve işle
ALL_USERS=$(fetch_all_users)
TOTAL=$(echo "$ALL_USERS" | jq 'length')
echo "Toplam $TOTAL kullanıcı çekildi"
# Sadece aktif kullanıcıları filtrele
ACTIVE_USERS=$(echo "$ALL_USERS" | jq '[.[] | select(.status == "active")]')
ACTIVE_COUNT=$(echo "$ACTIVE_USERS" | jq 'length')
echo "Aktif kullanıcı sayısı: $ACTIVE_COUNT"
# Admin kullanıcıların email listesini çıkar
echo "Admin kullanıcılar:"
echo "$ALL_USERS" | jq -r '.[] | select(.role == "admin") | .email'
Güvenlik Konuları
API ile çalışırken güvenlik göz ardı edilemez:
- API anahtarlarını asla script içine yazmayın: Environment variable veya secrets manager kullanın.
export API_KEY=$(vault kv get -field=key secret/api)gibi bir yaklaşım benimseyin. - Log dosyalarına hassas veri yazmayın: Response body’yi loglamadan önce token, password gibi alanları maskeleyin.
jq 'del(.password, .token, .secret)'ile bu alanları temizleyebilirsiniz. - SSL doğrulamasını devre dışı bırakmayın: Test ortamında bile
-kveya--insecurekullanmak kötü alışkanlık. Bunun yerine kendi CA sertifikanızı--cacertile belirtin. - Timeout değerlerini mutlaka ayarlayın:
--connect-timeoutve--max-timeolmadan bir API cevap vermezse script’iniz sonsuza kadar bekler. - Input validasyonu yapın: API’den gelen veriler shell’e geçmeden önce sanitize edin. Özellikle komut oluşturmada kullanılacak değerleri çift tırnak içinde kullanın.
Sonuç
API yanıtlarını düzgün işlemek sysadmin işinin ayrılmaz bir parçası haline geldi. Özetlemek gerekirse:
jqsenin en iyi arkadaşın, özellikle-rve// emptykombinasyonunu öğren- Her zaman HTTP status code’u kontrol et, yanıtın body’sini değil
- curl hataları ile HTTP hataları birbirinden farklı, ikisini ayrı handle et
- Retry mekanizması kur ama 4xx hataları için retry etme, boşuna kaynak harcamazsın
- Paginated endpoint’lerde tüm veriyi çektiğinden emin ol
- Temp file kullan, büyük response’ları değişkende tutmak bellek sorunlarına yol açabilir
- Null ve boş değerlere karşı savunmacı kod yaz, API’ler her zaman beklediğin alanı döndürmez
Bu yazıdaki örnekleri kendi ortamınıza uyarlayabilirsiniz. Önemli olan mantığı kavramak; hangi API olursa olsun, hata yönetimi ve veri doğrulama prensipleri değişmiyor.
