Serverless Fonksiyon İzleme ve Hata Ayıklama Rehberi
Serverless fonksiyonlar hayatımızı kolaylaştırıyor, bunu inkâr edemeyiz. Ancak bir şeyler ters gittiğinde, geleneksel sunucu ortamlarında alıştığımız “SSH aç, log’a bak, process’i kontrol et” döngüsü artık işe yaramıyor. Elinde tutunacak bir sunucu yok, çalışan bir process yok ve en kötüsü, hata nerede oldu diye baktığında karşına boş bir ekran çıkabiliyor. Bu yazıda AWS Lambda, Azure Functions ve Google Cloud Functions üzerinde gerçek dünya senaryolarıyla serverless izleme ve hata ayıklama konusunu derinlemesine ele alacağız.
Serverless İzlemenin Temel Zorlukları
Klasik bir VM ya da container ortamında bir uygulama patlıyorsa, sunucuya bağlanır, /var/log altına bakarsın, belki strace ile process’i takip edersin. Serverless’ta bu lüks yok. Fonksiyonlar milisaniyeler içinde çalışıp kayboluyor, her invocation ayrı bir ortamda çalışabilir ve “warm container” kavramı seni hem kurtarır hem de yanıltır.
En sık karşılaştığım problemler şunlar:
- Cold start gizemi: Fonksiyon neden bu kadar yavaş? Timeout mu yedi, yoksa sadece soğuk başlatma mı?
- Dağınık log’lar: Yüzlerce paralel invocation, log’ları neredeyse okunaksız hale getiriyor
- Görünmez bağımlılıklar: Dış servis çağrıları sessizce başarısız oluyor
- Memory leak tuzakları: Container yeniden kullanıldığında bir önceki invocation’ın bıraktığı kirli state
- Hata mesajlarının yetersizliği: “Task timed out after 30.00 seconds” yazan bir log sana hiçbir şey söylemiyor
Structured Logging: Her Şeyin Temeli
Serverless’ta en büyük hatalardan biri console.log("bir şeyler oldu") tarzında log atmak. Production’da böyle bir log’u gördüğünde ne yapacaksın? Hangi user, hangi request, hangi context?
Structured logging, yani JSON formatında ve bağlam içeren log’lar atmak, serverless’ta hayat kurtarır.
AWS Lambda’da Structured Logging
# AWS Lambda için Winston kurulumu
npm install winston
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: {
service: process.env.AWS_LAMBDA_FUNCTION_NAME,
version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
region: process.env.AWS_REGION
},
transports: [
new winston.transports.Console()
]
});
exports.handler = async (event, context) => {
const requestId = context.awsRequestId;
// Her log satırına request ID ekle
const log = logger.child({ requestId, correlationId: event.headers?.['x-correlation-id'] });
log.info('Fonksiyon başlatıldı', {
eventSource: event.source,
remainingTime: context.getRemainingTimeInMillis()
});
try {
const result = await processEvent(event, log);
log.info('İşlem tamamlandı', { success: true, resultCount: result.length });
return result;
} catch (error) {
log.error('İşlem başarısız', {
errorMessage: error.message,
errorStack: error.stack,
errorType: error.constructor.name
});
throw error;
}
};
Bu yaklaşımla CloudWatch’ta her log satırı JSON formatında geliyor ve kolayca filtrelenebiliyor. requestId ile belirli bir invocation’ın tüm log’larını tek sorguyla çekebilirsin.
Google Cloud Functions’da Structured Logging
GCP’de structured logging biraz farklı çalışıyor. severity alanını doğru set etmezsen, hata log’ların INFO olarak görünür ve alarm sistemleri tetiklenmez.
const { Logging } = require('@google-cloud/logging');
function createLogger(req) {
return {
info: (message, data = {}) => {
console.log(JSON.stringify({
severity: 'INFO',
message,
...data,
trace: req?.headers?.['x-cloud-trace-context']?.split('/')[0],
requestId: req?.headers?.['x-request-id']
}));
},
error: (message, data = {}) => {
console.error(JSON.stringify({
severity: 'ERROR',
message,
...data,
trace: req?.headers?.['x-cloud-trace-context']?.split('/')[0]
}));
},
warning: (message, data = {}) => {
console.warn(JSON.stringify({
severity: 'WARNING',
message,
...data
}));
}
};
}
exports.processOrder = async (req, res) => {
const log = createLogger(req);
log.info('Sipariş isteği alındı', {
orderId: req.body?.orderId,
userId: req.body?.userId,
itemCount: req.body?.items?.length
});
// ... işlemler
};
CloudWatch Insights ile Derinlemesine Analiz
AWS kullanıyorsan CloudWatch Logs Insights, serverless debugging’in en güçlü silahlarından biri. Düzgün kullanmayı öğrenirsen saatler kazanırsın.
Temel Insights Sorguları
# Son 1 saatte timeout yiyen fonksiyonları bul
fields @timestamp, @requestId, @message
| filter @message like /Task timed out/
| sort @timestamp desc
| limit 20
# Ortalama duration ve cold start oranını hesapla
filter @type = "REPORT"
| stats
avg(@duration) as avgDuration,
max(@duration) as maxDuration,
avg(@initDuration) as avgColdStart,
count(*) as totalInvocations,
sum(@initDuration > 0) as coldStarts
| fields coldStarts / totalInvocations * 100 as coldStartPercent
# Belirli bir hata tipini tetikleyen request'leri bul
fields @timestamp, @requestId, @message, correlationId
| filter severity = "ERROR" and errorType = "DatabaseConnectionError"
| sort @timestamp desc
| limit 50
Bu sorgular özellikle “production’da aralıklı hata var ama reproduce edemiyorum” durumlarında çok işe yarıyor. Request ID’yi alıp tüm invocation’ın log’larını çekebilirsin.
Distributed Tracing: Karanlıkta Harita
Bir API çağrısı 5 farklı Lambda fonksiyonunu tetikliyorsa ve bir yerde hata varsa, hangisinde olduğunu nasıl bulursun? Distributed tracing tam bu noktada devreye giriyor.
AWS X-Ray Entegrasyonu
const AWSXRay = require('aws-xray-sdk-core');
const AWS = AWSXRay.captureAWS(require('aws-sdk'));
const https = AWSXRay.captureHTTPs(require('https'));
exports.handler = async (event, context) => {
// X-Ray segment'i al
const segment = AWSXRay.getSegment();
// Özel subsegment oluştur
const dbSubsegment = segment.addNewSubsegment('DatabaseQuery');
try {
const dynamodb = new AWS.DynamoDB.DocumentClient();
const params = {
TableName: process.env.TABLE_NAME,
Key: { userId: event.userId }
};
const result = await dynamodb.get(params).promise();
// Subsegment'e metadata ekle
dbSubsegment.addMetadata('queryParams', params);
dbSubsegment.addAnnotation('userId', event.userId);
dbSubsegment.close();
// Dış API çağrısı - otomatik olarak trace edilir
const apiSubsegment = segment.addNewSubsegment('ExternalAPI');
const response = await callExternalAPI(result.Item);
apiSubsegment.addMetadata('apiResponse', { statusCode: response.status });
apiSubsegment.close();
return response.data;
} catch (error) {
dbSubsegment.addError(error);
dbSubsegment.close();
throw error;
}
};
X-Ray’in güzel yanı, tüm trace’i görsel olarak görmeni sağlaması. DynamoDB sorgusunun 200ms, dış API çağrısının 1.8 saniye sürdüğünü ve toplam timeout’un neden oluştuğunu tek ekranda görebilirsin.
Alarm ve Alert Stratejileri
İzleme yapmak yetmez, kritik durumlarda haberdar edilmen gerekir. Ama her hata için alarm kurarsan alarm yorgunluğu yaşarsın ve gerçek kritik hataları kaçırırsın.
Lambda için Temel Metrikler
Şunlara mutlaka alarm kur:
- Errors: Fonksiyonun döndürdüğü hata sayısı
- Throttles: Concurrency limitine çarpma sayısı
- Duration (P99): En yavaş %1’lik invocation süresi
- ConcurrentExecutions: Eş zamanlı çalışan fonksiyon sayısı
- DeadLetterErrors: SQS/SNS dead letter queue’ya düşen mesajlar
# AWS CLI ile Lambda alarm kurma
aws cloudwatch put-metric-alarm
--alarm-name "Lambda-OrderProcessor-HighErrorRate"
--alarm-description "Order processor hata orani yuksek"
--metric-name Errors
--namespace AWS/Lambda
--statistic Sum
--dimensions Name=FunctionName,Value=order-processor
--period 300
--evaluation-periods 2
--threshold 5
--comparison-operator GreaterThanThreshold
--treat-missing-data notBreaching
--alarm-actions arn:aws:sns:eu-west-1:123456789:ops-alerts
--ok-actions arn:aws:sns:eu-west-1:123456789:ops-alerts
Composite Alarm Kullanımı
Tek metrik yerine birden fazla metriği birleştiren composite alarm’lar, false positive oranını dramatik şekilde düşürür.
# Önce bireysel alarm'ları oluştur
aws cloudwatch put-metric-alarm
--alarm-name "Lambda-Errors-Individual"
--metric-name Errors
--namespace AWS/Lambda
--statistic Sum
--period 60
--evaluation-periods 1
--threshold 3
--comparison-operator GreaterThanThreshold
--dimensions Name=FunctionName,Value=payment-service
# Sonra composite alarm oluştur
aws cloudwatch put-composite-alarm
--alarm-name "Lambda-PaymentService-Critical"
--alarm-description "Odeme servisi kritik durum"
--alarm-rule "ALARM(Lambda-Errors-Individual) AND ALARM(Lambda-Duration-High)"
--alarm-actions arn:aws:sns:eu-west-1:123456789:pagerduty-critical
Gerçek Dünya Senaryosu: Aralıklı Timeout Problemi
Geçen ay bir e-ticaret projemizde ilginç bir sorun yaşadık. Ödeme işlemlerini yürüten Lambda fonksiyonu, günün belirli saatlerinde timeout yiyordu ama her seferinde değil, rastgele görünüyordu. Log’lara bakınca şu paterni fark ettim:
Timeout yaşanan invocation’ların büyük çoğunluğu, önceki invocation’dan 15 dakikadan fazla süre geçmişti. Bu klasik cold start sorunu değildi çünkü init duration normal görünüyordu. Sorun, fonksiyonun global scope’ta tuttuğu veritabanı bağlantısıydı.
// YANLIŞ YAPILANMA
const { Pool } = require('pg');
// Global scope'da bağlantı - container "warm" ama bağlantı kopmuş olabilir
const pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
// connectionTimeout yok!
});
exports.handler = async (event) => {
// Bağlantı zaten kopmuşsa, bu satır uzun süre bekler
const client = await pool.connect();
// ...
};
// DOĞRU YAPILANMA
const { Pool } = require('pg');
let pool = null;
function getPool() {
if (!pool) {
pool = new Pool({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
connectionTimeoutMillis: 5000,
idleTimeoutMillis: 10000,
max: 2, // Lambda için düşük tut
});
// Bağlantı hatasında pool'u sıfırla
pool.on('error', (err) => {
console.error(JSON.stringify({
severity: 'ERROR',
message: 'Database pool error',
error: err.message
}));
pool = null;
});
}
return pool;
}
exports.handler = async (event, context) => {
const remainingMs = context.getRemainingTimeInMillis();
if (remainingMs < 3000) {
console.warn(JSON.stringify({
severity: 'WARNING',
message: 'Kalan sure az, erken cikis',
remainingMs
}));
throw new Error('Insufficient time remaining');
}
const pool = getPool();
const client = await pool.connect();
try {
await client.query('BEGIN');
// işlemler
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
Bu değişiklikle timeout problemi tamamen ortadan kalktı. idleTimeoutMillis sayesinde boşta kalan bağlantılar otomatik kapatılıyor ve bir sonraki invocation temiz bir bağlantı alıyor.
Lokal Geliştirme ve Debug Ortamı Kurma
“Ama production’da test edemem ki” diyorsan, lokal ortamda gerçekçi bir test ortamı kurmak mümkün.
# AWS SAM CLI ile lokal Lambda ortamı
npm install -g aws-sam-cli
# SAM template.yaml oluştur
cat > template.yaml << 'EOF'
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
MemorySize: 256
Environment:
Variables:
LOG_LEVEL: debug
NODE_ENV: local
Resources:
OrderProcessor:
Type: AWS::Serverless::Function
Properties:
Handler: src/handler.main
Runtime: nodejs18.x
Events:
Api:
Type: Api
Properties:
Path: /order
Method: post
EOF
# Lokal API başlat
sam local start-api --debug-port 5858
# Veya doğrudan fonksiyon invoke et
sam local invoke OrderProcessor
--event events/order-event.json
--debug-port 5858
--env-vars env.json
# VS Code ile remote debugging için launch.json
cat > .vscode/launch.json << 'EOF'
{
"version": "0.2.0",
"configurations": [
{
"name": "SAM Lambda Debug",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 5858,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/task",
"protocol": "inspector",
"stopOnEntry": false
}
]
}
EOF
Bu setup ile breakpoint koyabilir, variable’ları inceleyebilir ve adım adım kodu takip edebilirsin. Production ortamında asla yapamayacağın şeyleri lokal’de rahatlıkla yapıyorsun.
Dead Letter Queue ile Hata Yönetimi
Asenkron Lambda invocation’larında bir hata oluştuğunda ne oluyor? Lambda varsayılan olarak 2 kez tekrar deniyor ve sonra olayı siliyor. Bu durumda hem hatayı hem de kaybolan veriyi geri getirmen neredeyse imkânsız.
# SQS Dead Letter Queue oluştur
aws sqs create-queue
--queue-name order-processor-dlq
--attributes '{
"MessageRetentionPeriod": "1209600",
"ReceiveMessageWaitTimeSeconds": "20"
}'
# Lambda'ya DLQ ekle
aws lambda put-function-event-invoke-config
--function-name order-processor
--maximum-retry-attempts 2
--maximum-event-age-in-seconds 3600
--destination-config '{
"OnFailure": {
"Destination": "arn:aws:sqs:eu-west-1:123456789:order-processor-dlq"
},
"OnSuccess": {
"Destination": "arn:aws:sqs:eu-west-1:123456789:order-success-queue"
}
}'
DLQ’daki mesajları periyodik olarak işlemek için ayrı bir Lambda yazabilirsin:
exports.processDLQ = async (event) => {
for (const record of event.Records) {
const originalMessage = JSON.parse(record.body);
const requestContext = originalMessage.requestContext;
console.log(JSON.stringify({
severity: 'ERROR',
message: 'DLQ mesaji isleniyor',
originalFunction: requestContext?.functionName,
originalRequestId: requestContext?.requestId,
errorMessage: requestContext?.condition,
payload: originalMessage.requestPayload,
retryCount: record.attributes?.ApproximateReceiveCount
}));
// Opsiyonel: Belirli hata tiplerini yeniden işlemeye çalış
if (requestContext?.condition !== 'RetriesExhausted') {
await reprocessMessage(originalMessage.requestPayload);
}
// Her durumda alert gönder
await sendAlert({
type: 'DLQ_MESSAGE',
details: originalMessage
});
}
};
Performans İzleme ve Cold Start Optimizasyonu
Cold start’ı tamamen ortadan kaldıramazsın ama etkisini minimize edebilirsin. Önce ölçmen gerekiyor.
# Son 24 saatte cold start istatistiklerini çek
aws logs insights start-query
--log-group-name /aws/lambda/order-processor
--start-time $(date -d '24 hours ago' +%s)
--end-time $(date +%s)
--query-string '
filter @type = "REPORT"
| stats
count(*) as total,
sum(@initDuration > 0) as coldStarts,
avg(@initDuration) as avgColdStartMs,
avg(@duration) as avgDurationMs,
percentile(@duration, 99) as p99Duration
'
Cold start süresini düşürmek için birkaç temel önlem:
- Bağımlılık sayısını azalt: Her
require()çağrısı cold start’a ms ekler - Tree shaking kullan: Tüm SDK yerine sadece ihtiyacın olan servisi import et
- Provisioned Concurrency: Kritik fonksiyonlar için warm instance’lar hazırda beklesin
- Lazy initialization: Global scope’da her şeyi başlatmak yerine, ilk gerçekten ihtiyaç duyulduğunda başlat
// YANLIŞ: Tüm SDK'yı import et
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const dynamodb = new AWS.DynamoDB.DocumentClient();
const ses = new AWS.SES();
const sns = new AWS.SNS();
// DOĞRU: Sadece ihtiyacın olanı import et, lazy load yap
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
let s3Client = null;
function getS3Client() {
if (!s3Client) {
s3Client = new S3Client({ region: process.env.AWS_REGION });
}
return s3Client;
}
Observability Stack: Her Şeyi Bir Araya Getirmek
Büyük sistemlerde tek başına CloudWatch veya tek başına X-Ray yetmez. Grafana + Prometheus + OpenTelemetry kombinasyonu çok daha güçlü bir görünürlük sağlar.
// OpenTelemetry ile Lambda instrumentation
const { NodeTracerProvider } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.AWS_LAMBDA_FUNCTION_NAME,
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.AWS_LAMBDA_FUNCTION_VERSION,
'cloud.provider': 'aws',
'cloud.region': process.env.AWS_REGION,
'faas.name': process.env.AWS_LAMBDA_FUNCTION_NAME
})
});
const exporter = new OTLPTraceExporter({
url: process.env.OTLP_ENDPOINT,
headers: {
'x-honeycomb-team': process.env.HONEYCOMB_API_KEY
}
});
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
const tracer = provider.getTracer('lambda-tracer');
Sonuç
Serverless debugging, geleneksel yaklaşımları tamamen bir kenara bırakıp yeni bir zihinsel modele geçmeyi gerektiriyor. En önemli çıkarımlar şunlar:
Structured logging yapma, önceden planla. Her log satırının context ID, user ID, operation tipi ve severity içermesi gerekiyor. Sonradan eklemek çok daha zahmetli.
Distributed tracing opsiyonel değil, zorunlu. Birden fazla fonksiyon içeren herhangi bir sistem için X-Ray veya OpenTelemetry gibi bir tracing çözümü şart. Aksi takdirde hata hangi adımda oluştu sorusuna asla kesin cevap veremezsin.
Dead letter queue’ları ihmal etme. Kayıp veri, timeout’tan çok daha büyük bir problem. Her asenkron fonksiyon için DLQ konfigüre et ve o queue’yu düzenli izle.
Lokal geliştirme ortamını production’a yaklaştır. SAM CLI veya Serverless Framework’ün offline eklentisi ile lokal’de gerçekçi bir ortam kurabilirsin. Bu hem debug sürecini hızlandırır hem de “production’da çalışıyor mu bilmiyorum” belirsizliğini ortadan kaldırır.
Alarm yorgunluğuna dikkat et. Her metrik için alarm kurmak yerine, gerçekten aksiyon alman gereken durumlar için composite alarm’lar tasarla. Gece 3’te gereksiz yere uyandırılan bir ekip, zamanla alarmları kapatmaya başlar, bu da gerçek kritik hataları kaçırmana neden olur.
Serverless’ın soyutladığı karmaşıklık, izleme ve debugging tarafında katmerli geri dönüyor. Ama doğru araçları ve yaklaşımları kullandığında, bu karmaşıklığı yönetilebilir kılmak mümkün.
