İçeriğe atla

AWS Lambda Production Monitoring ve Debugging: Kanıtlanmış Stratejiler

AWS Lambda'yı production için enstrümente edin: CloudWatch custom metrics, X-Ray tracing, structured logging ve business etkisini izleyen alert'ler.

Ayhan Sipahi Ayhan Sipahi

Lambda function’ları production’da sessizce başarısız olur; varsayılan CloudWatch metrikleri, cold-start gürültüsü ve eksik distributed trace’ler bir araya geldiğinde kök nedeni gizler. Yapılandırılmış bir observability stratejisi olmadan debugging, veri odaklı analizden baskı altında tahmine kayar. Çoğu Lambda iş yükünde işe yarayan kurulum küçüktür: business sonuçlarına bağlanmış custom metrikler, X-Ray active tracing ve her satırında request ID taşıyan JSON log’lar.

Bu üç sinyal farklı işleri görür. Metrikler bir şeyin bozulduğunu, trace’ler nerede bozulduğunu, log’lar ise nedenini söyler. Birini eksik bıraktığınızda diğer ikisi en kötü anda ortaya çıkan bir boşluk bırakır: hata log’lamadan timeout’a düşen bir ödeme handler’ı ya da hiçbir alarmı tetiklemeyen bir retry fırtınası.

Lambda Observability’nin Üç Direği#

1. Metrikler: Erken Uyarı Sistemi#

Mutlaka Monitor Etmeniz Gereken Metrikler:

// Lambda observability için custom metrikler
// Node.js 20.x ve 22.x runtime'ları ile uyumlu
import { CloudWatch } from '@aws-sdk/client-cloudwatch';

const cloudwatch = new CloudWatch({});

export const publishCustomMetrics = async (
  functionName: string,
  duration: number,
  success: boolean,
  businessContext?: { userId?: string, feature?: string }
) => {
  const metrics = [
    {
      MetricName: 'FunctionDuration',
      Value: duration,
      Unit: 'Milliseconds',
      Dimensions: [
        { Name: 'FunctionName', Value: functionName },
        { Name: 'Feature', Value: businessContext?.feature || 'unknown' }
      ]
    },
    {
      MetricName: success ? 'FunctionSuccess' : 'FunctionFailure',
      Value: 1,
      Unit: 'Count',
      Dimensions: [
        { Name: 'FunctionName', Value: functionName }
      ]
    }
  ];

  // Business-specific metrikler. userId'yi dimension'lardan uzak tutun:
  // her farklı dimension kombinasyonu ayrı bir custom metric olarak
  // faturalanır, yani bir user ID tek metriği kullanıcı başına bir metriğe çevirir.
  if (businessContext?.userId) {
    metrics.push({
      MetricName: 'UserAction',
      Value: 1,
      Unit: 'Count',
      Dimensions: [
        { Name: 'FunctionName', Value: functionName },
        { Name: 'ActionType', Value: success ? 'completed' : 'failed' }
      ]
    });
  }

  await cloudwatch.putMetricData({
    Namespace: 'Lambda/Business',
    MetricData: metrics
  });
};

2. Trace’ler: İstek Akışı#

X-Ray, kendi enstrümente etmediğiniz servislere giden çağrılar dahil tüm istek yolunu gösterir:

import AWSXRay from 'aws-xray-sdk-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';

// AWS SDK v3'u instrument et
const dynamoClient = AWSXRay.captureAWSv3Client(new DynamoDBClient({}));
const dynamoDB = DynamoDBDocumentClient.from(dynamoClient);

// Active tracing açıkken segment'i handler çalışmadan önce Lambda açar.
// captureAsyncFunc callback'ini hemen çalıştırır; bu yüzden handler'ın
// içinde belirli bir çağrının etrafında kullanılır, export'un etrafında değil.
export const handler = async (event: any) => {
  // Filtreleme için custom annotation'lar ekle
  const segment = AWSXRay.getSegment();
  segment?.addAnnotation('userId', event.userId);
  segment?.addAnnotation('paymentMethod', event.paymentMethod);
  segment?.addAnnotation('environment', process.env.STAGE);

  // External API call'ları trace et
  const subsegment = segment?.addNewSubsegment('payment-provider-api');

  try {
    const paymentResult = await processPayment(event);
    subsegment?.close();

    // Business metadata ekle
    segment?.addMetadata('payment', {
      amount: event.amount,
      currency: event.currency,
      processingTime: Date.now() - event.timestamp
    });

    return { success: true, paymentId: paymentResult.id };
  } catch (error) {
    // Hatayla kapatmak subsegment'in açık kalmasını engeller
    subsegment?.close(error as Error);
    segment?.addError(error as Error);
    segment?.addMetadata('errorContext', {
      userId: event.userId,
      errorType: (error as Error).name,
      requestId: event.requestId
    });
    throw error;
  }
};

3. Log’lar: Tarihsel Kayıt#

İşe Yarar Structured Logging Pattern’i:

import { createLogger, format, transports } from 'winston';

const logger = createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: format.combine(
    format.timestamp(),
    format.errors({ stack: true }),
    format.json()
  ),
  transports: [
    new transports.Console()
  ]
});

// Lambda context-aware logging
export const createContextLogger = (context: any, event: any) => {
  const requestId = context.awsRequestId;
  const functionName = context.functionName;
  
  return {
    info: (message: string, meta?: any) => logger.info({
      message,
      requestId,
      functionName,
      stage: process.env.STAGE,
      ...meta
    }),
    
    error: (message: string, error?: Error, meta?: any) => logger.error({
      message,
      error: error?.stack || error?.message,
      requestId,
      functionName,
      stage: process.env.STAGE,
      ...meta
    }),
    
    // Business event logging
    business: (event: string, data: any) => logger.info({
      message: `Business Event: ${event}`,
      businessEvent: event,
      data,
      requestId,
      functionName,
      timestamp: new Date().toISOString()
    })
  };
};

// Handler'da kullanım
export const handler = async (event: any, context: any) => {
  const log = createContextLogger(context, event);
  
  log.info('Function invoked', { eventType: event.Records?.[0]?.eventName });
  
  try {
    const result = await processEvent(event);
    log.business('order-processed', { orderId: result.orderId, amount: result.amount });
    return result;
  } catch (error) {
    log.error('Processing failed', error as Error, { eventData: event });
    throw error;
  }
};

CloudWatch Dashboard Kurulumu#

Stakeholder İletişimi için Business Dashboard#

Stakeholder’lar sistem sağlığı konusunda görünürlük istediğinde, teknik detaylardan çok business odaklı metrikleri göstermek daha değerli oluyor:

# Business odaklı dashboard için CloudFormation template
Resources:
  BusinessDashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: "Lambda-Business-Health"
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "properties": {
                "metrics": [
                  ["Lambda/Business", "OrdersProcessed", "FunctionName", "order-processor"],
                  ["Lambda/Business", "PaymentsCompleted", "FunctionName", "payment-processor"],
                  ["Lambda/Business", "UserRegistrations", "FunctionName", "user-registration"]
                ],
                "period": 300,
                "stat": "Sum",
                "region": "${AWS::Region}",
                "title": "Business Transactions (Last 24h)"
              }
            },
            {
              "type": "metric",
              "properties": {
                "metrics": [
                  ["AWS/Lambda", "Errors", "FunctionName", "order-processor"],
                  ["AWS/Lambda", "Throttles", "FunctionName", "payment-processor"]
                ],
                "period": 300,
                "stat": "Sum",
                "region": "${AWS::Region}",
                "title": "System Health Issues"
              }
            }
          ]
        }

Debugging için Teknik Dashboard#

  TechnicalDashboard:
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: "Lambda-Technical-Deep-Dive"
      DashboardBody: !Sub |
        {
          "widgets": [
            {
              "type": "metric",
              "properties": {
                "metrics": [
                  ["AWS/Lambda", "Duration", "FunctionName", "payment-processor", { "stat": "Average" }],
                  ["AWS/Lambda", "Duration", "FunctionName", "payment-processor", { "stat": "p99" }]
                ],
                "period": 60,
                "region": "${AWS::Region}",
                "title": "Function Duration (Average vs P99)"
              }
            },
            {
              "type": "log",
              "properties": {
                "query": "SOURCE '/aws/lambda/payment-processor'\n| fields @timestamp, @message, @requestId\n| filter @message like /ERROR/\n| sort @timestamp desc\n| limit 100",
                "region": "${AWS::Region}",
                "title": "Recent Errors (Last 1 Hour)"
              }
            }
          ]
        }

Alerting Stratejileri#

Business Etkisine Dayalı Alert’ler#

Her şeye değil, business etkisine alert verin:

# CloudFormation alert konfigürasyonu
Resources:
  # Kritik: Ödeme işleme hataları
  PaymentFailureAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: "Lambda-PaymentProcessor-CriticalFailures"
      AlarmDescription: "Payment processing failures above threshold"
      MetricName: Errors
      Namespace: AWS/Lambda
      Statistic: Sum
      Period: 300
      EvaluationPeriods: 2
      Threshold: 5  # 10 dakikada 5'ten fazla hata
      ComparisonOperator: GreaterThanThreshold
      Dimensions:
        - Name: FunctionName
          Value: !Ref PaymentProcessorFunction
      AlarmActions:
        - !Ref CriticalAlertTopic
      TreatMissingData: notBreaching

  # Uyarı: Normalden yavaş işlem
  PaymentLatencyAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: "Lambda-PaymentProcessor-HighLatency"
      MetricName: Duration
      Namespace: AWS/Lambda
      Statistic: Average
      Period: 300
      EvaluationPeriods: 3
      Threshold: 5000  # 5 saniye ortalama
      ComparisonOperator: GreaterThanThreshold
      AlarmActions:
        - !Ref WarningAlertTopic

  # Genel sistem sağlığı için composite alarm
  SystemHealthAlarm:
    Type: AWS::CloudWatch::CompositeAlarm
    Properties:
      AlarmName: "Lambda-SystemHealth-Critical"
      AlarmRule: !Sub |
        ALARM("${PaymentFailureAlarm}") OR 
        ALARM("${OrderProcessingAlarm}") OR
        ALARM("${DatabaseConnectionAlarm}")
      AlarmActions:
        - !Ref EmergencyAlertTopic

Timeout’a Yaklaşma Tespiti#

Timeout’lar Errors metriğine yansımadan çok önce Duration sıçraması olarak görünür. Bu metrik, invocation’ın raporlayacak bütçesi hâlâ varken tetiklenir:

export const detectNearTimeout = async (functionName: string, context: any) => {
  const remainingTime = context.getRemainingTimeInMillis();

  // Bir saniyeden az bütçe kaldı: yavaş bir dependency çağrısı invocation'ı bitirir
  if (remainingTime < 1000) {
    await cloudwatch.putMetricData({
      Namespace: 'Lambda/Performance',
      MetricData: [{
        MetricName: 'NearTimeout',
        Value: 1,
        Unit: 'Count',
        // remainingTime dimension'a girmez; milisaniye değeri olarak
        // neredeyse her invocation'da yeni bir custom metric yaratır
        Dimensions: [
          { Name: 'FunctionName', Value: functionName }
        ]
      }]
    });
  }
};

Error Handling ve Dead Letter Queue’lar#

Stratejik Error Handling#

// Daha iyi debugging için error kategorilendirmesi
export enum ErrorCategory {
  TRANSIENT = 'TRANSIENT',  // Retry mantıklı
  CLIENT_ERROR = 'CLIENT_ERROR', // Kullanıcı input sorunu
  SYSTEM_ERROR = 'SYSTEM_ERROR', // Altyapı problemi
  BUSINESS_ERROR = 'BUSINESS_ERROR' // Business logic ihlali
}

export class CategorizedError extends Error {
  constructor(
    message: string,
    public category: ErrorCategory,
    public retryable: boolean = false,
    public context?: any
  ) {
    super(message);
    this.name = 'CategorizedError';
  }
}

export const handleError = async (error: Error, event: any, context: any) => {
  const log = createContextLogger(context, event);
  
  if (error instanceof CategorizedError) {
    // Kategorilere göre error handle et
    switch (error.category) {
      case ErrorCategory.TRANSIENT:
        log.info('Transient error - will retry', { 
          error: error.message, 
          retryable: error.retryable 
        });
        throw error; // Lambda retry mekanizmasının handle etmesini sağla
        
      case ErrorCategory.CLIENT_ERROR:
        log.info('Client error - no retry needed', { error: error.message });
        return { 
          statusCode: 400, 
          body: JSON.stringify({ error: 'Invalid request' })
        };
        
      case ErrorCategory.SYSTEM_ERROR:
        log.error('System error detected', error, { 
          requiresInvestigation: true 
        });
        // İnceleme için DLQ'ya gönder
        throw error;
        
      case ErrorCategory.BUSINESS_ERROR:
        log.business('business-rule-violation', {
          rule: error.message,
          context: error.context
        });
        return {
          statusCode: 422,
          body: JSON.stringify({ error: error.message })
        };
    }
  } else {
    // Bilinmeyen error - system error olarak ele al
    log.error('Uncategorized error', error);
    throw new CategorizedError(
      error.message,
      ErrorCategory.SYSTEM_ERROR,
      false,
      { originalError: error.stack }
    );
  }
};

Dead Letter Queue Analizi#

// Error pattern analizi için DLQ processor
export const dlqProcessor = async (event: any, context: any) => {
  const log = createContextLogger(context, event);
  
  for (const record of event.Records) {
    try {
      const failedEvent = JSON.parse(record.body);
      const errorInfo = {
        functionName: record.eventSourceARN?.split(':')[6],
        errorCount: record.attributes?.ApproximateReceiveCount || '1',
        // Lambda başarısız async event'i DLQ'ya yazarken RequestID, ErrorCode
        // ve ErrorMessage'ı SQS message attribute'u olarak ekler
        errorCode: record.messageAttributes?.ErrorCode?.stringValue || 'unknown',
        failureReason: record.messageAttributes?.ErrorMessage?.stringValue || 'unknown',
        originalTimestamp: failedEvent.timestamp,
        retryCount: parseInt(record.attributes?.ApproximateReceiveCount || '0')
      };
      
      // Pattern detection
      if (errorInfo.retryCount > 3) {
        log.business('recurring-failure-pattern', {
          pattern: 'high-retry-count',
          functionName: errorInfo.functionName,
          suggestion: 'investigate-configuration'
        });
      }
      
      // Analiz için sakla
      await storeErrorPattern(errorInfo, failedEvent);
      
    } catch (processingError) {
      log.error('Failed to process DLQ record', processingError as Error);
    }
  }
};

İleri Seviye Debugging Teknikleri#

Lambda Function URL Debugging#

// Production troubleshooting için debug endpoint
export const debugHandler = async (event: any, context: any) => {
  // Sadece non-production'da veya özel header ile izin ver
  const allowDebug = process.env.STAGE !== 'prod' || 
                     event.headers?.['x-debug-token'] === process.env.DEBUG_TOKEN;
  
  if (!allowDebug) {
    return { statusCode: 403, body: 'Debug access denied' };
  }
  
  const debugInfo = {
    environment: {
      stage: process.env.STAGE,
      region: context.invokedFunctionArn.split(':')[3],
      memorySize: context.memoryLimitInMB,
      remainingTimeMs: context.getRemainingTimeInMillis()
    },
    runtime: {
      nodeVersion: process.version,
      platform: process.platform,
      uptime: process.uptime()
    },
    lastErrors: await getRecentErrors(context.functionName),
    healthChecks: {
      database: await checkDatabaseConnection(),
      externalAPI: await checkExternalServices(),
      memory: process.memoryUsage()
    }
  };
  
  return {
    statusCode: 200,
    body: JSON.stringify(debugInfo, null, 2)
  };
};

Production’da Performance Profiling#

// Güvenli production profiling
export const profileHandler = (originalHandler: Function) => {
  return async (event: any, context: any) => {
    const shouldProfile = Math.random() < 0.01; // Request'lerin %1'ini profile et
    
    if (!shouldProfile) {
      return originalHandler(event, context);
    }
    
    const startTime = Date.now();
    const startMemory = process.memoryUsage();
    
    try {
      const result = await originalHandler(event, context);
      
      const endTime = Date.now();
      const endMemory = process.memoryUsage();
      
      // Profiling datasını gönder
      await cloudwatch.putMetricData({
        Namespace: 'Lambda/Profiling',
        MetricData: [
          {
            MetricName: 'ExecutionDuration',
            Value: endTime - startTime,
            Unit: 'Milliseconds'
          },
          {
            MetricName: 'MemoryUsed',
            Value: endMemory.heapUsed - startMemory.heapUsed,
            Unit: 'Bytes'
          }
        ]
      });
      
      return result;
    } catch (error) {
      // Error senaryolarını da profile et
      const errorTime = Date.now();
      await cloudwatch.putMetricData({
        Namespace: 'Lambda/Profiling',
        MetricData: [{
          MetricName: 'ErrorDuration',
          Value: errorTime - startTime,
          Unit: 'Milliseconds'
        }]
      });
      throw error;
    }
  };
};

Troubleshooting Workflow’ları#

5 Dakikalık Debug Protokolü#

Peak trafik sırasında işler ters gittiğinde sistematik bir yaklaşıma ihtiyacınız var:

// Acil durum debug checklist'i
export const emergencyDebugChecklist = {
  step1_quickHealth: async (functionName: string) => {
    const metrics = await cloudwatch.getMetricStatistics({
      Namespace: 'AWS/Lambda',
      MetricName: 'Errors',
      Dimensions: [{ Name: 'FunctionName', Value: functionName }],
      StartTime: new Date(Date.now() - 10 * 60 * 1000), // Son 10 dakika
      EndTime: new Date(),
      Period: 300,
      Statistics: ['Sum']
    });
    
    return {
      recentErrors: metrics.Datapoints?.reduce((sum, dp) => sum + (dp.Sum || 0), 0),
      timeframe: 'last-10-minutes'
    };
  },
  
  step2_checkDependencies: async () => {
    return {
      database: await checkDatabaseConnection(),
      externalAPIs: await checkExternalServices(),
      downstream: await checkDownstreamServices()
    };
  },
  
  step3_analyzeLogs: async (functionName: string) => {
    // Son hatalar için CloudWatch Logs Insights query
    const query = `
      fields @timestamp, @message, @requestId
      | filter @message like /ERROR/ or @message like /TIMEOUT/
      | sort @timestamp desc
      | limit 20
    `;
    
    // Implementation CloudWatch Logs API kullanacak
    return { recentErrorPatterns: 'implementation-needed' };
  }
};

Memory Leak Detection#

// Uzun çalışan Lambda container'larında memory leak tespiti
let requestCount = 0;
const memorySnapshots: Array<{ count: number; memory: NodeJS.MemoryUsage }> = [];

export const memoryTrackingWrapper = (handler: Function) => {
  return async (event: any, context: any) => {
    requestCount++;
    
    const beforeMemory = process.memoryUsage();
    const result = await handler(event, context);
    const afterMemory = process.memoryUsage();
    
    // Request'ler boyunca memory artışını takip et
    if (requestCount % 10 === 0) {
      memorySnapshots.push({ count: requestCount, memory: afterMemory });
      // Buffer'ı sınırla ki leak dedektörü kendisi leak yapmasın
      if (memorySnapshots.length > 20) memorySnapshots.shift();
      
      if (memorySnapshots.length > 10) {
        const oldSnapshot = memorySnapshots[memorySnapshots.length - 10];
        const currentSnapshot = memorySnapshots[memorySnapshots.length - 1];
        
        const heapGrowth = currentSnapshot.memory.heapUsed - oldSnapshot.memory.heapUsed;
        
        if (heapGrowth > 50 * 1024 * 1024) { // 50MB artış
          await cloudwatch.putMetricData({
            Namespace: 'Lambda/MemoryLeak',
            MetricData: [{
              MetricName: 'SuspectedMemoryLeak',
              Value: heapGrowth,
              Unit: 'Bytes',
              Dimensions: [
                { Name: 'FunctionName', Value: context.functionName }
              ]
            }]
          });
        }
      }
    }
    
    return result;
  };
};

Maliyete Duyarlı Monitoring#

Yüksek Hacimli Function’lar için Sampling Stratejisi#

// Business değerine dayalı akıllı sampling
export const createSampler = (baseSampleRate: number = 0.01) => {
  return (event: any): boolean => {
    // Her zaman error'ları sample et
    if (event.errorType) return true;
    
    // Her zaman yüksek değerli transaction'ları sample et
    if (event.transactionValue > 1000) return true;
    
    // Yeni kullanıcıları daha sık sample et
    if (event.userType === 'new') return Math.random() < baseSampleRate * 5;
    
    // Normal sampling
    return Math.random() < baseSampleRate;
  };
};

const sampler = createSampler(0.005); // %0.5 base rate

export const handler = async (event: any, context: any) => {
  const shouldMonitor = sampler(event);
  
  if (shouldMonitor) {
    // Tam monitoring ve tracing
    return AWSXRay.captureAsyncFunc('handler', async () => {
      return processWithFullLogging(event, context);
    });
  } else {
    // Minimal monitoring
    return processWithBasicLogging(event, context);
  }
};

Log Retention Stratejisi#

# Log önemine göre farklı retention süresi
Resources:
  BusinessLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${BusinessProcessorFunction}"
      RetentionInDays: 90  # Business log'ları daha uzun tut
      
  DebugLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${UtilityFunction}"
      RetentionInDays: 7  # Debug log'ları daha kısa olabilir

Bir Sonraki Bölüm: Advanced Pattern’ler ve Maliyet Optimizasyonu#

Bu serinin son bölümü, hem karmaşıklığı hem de maliyeti azaltan advanced Lambda pattern’lerini ele alıyor:

  • Multi-tenant mimari pattern’leri
  • Event-driven maliyet optimizasyonu
  • Advanced deployment stratejileri
  • Performance vs maliyet trade-off’ları

Bu Kurulum Ne Zaman Yeterli#

Custom business metrikleri, active X-Ray tracing ve JSON log’lar çoğu Lambda iş yükünde neyin, nerede ve neden bozulduğunu üçüncü parti bir agent çalıştırmadan yanıtlar. İki durum fazlasını gerektirir. Bir istek çok sayıda servisten geçiyorsa ve X-Ray sampling ihtiyacınız olan trace’i sürekli düşürüyorsa, ADOT Lambda layer’ı üzerinden OpenTelemetry’ye geçin ve span’ları hepsini saklayan bir backend’e gönderin. Senkron putMetricData çağrısı function duration’ınızda görünmeye başladıysa Embedded Metric Format’a geçin: metrik zaten yazdığınız bir log satırının içinde taşınır ve invocation’ı bloklamaz. Custom metric ücretleri her iki yolda da aynı kalır, yani cardinality disiplini yine geçerlidir. Üç sinyalle başlayın; bu ikisini ancak belirli bir soru yanıtsız kalmaya devam ettiğinde ekleyin.

Kaynaklar#

AWS Lambda Production Rehberi: 5 Yıllık Gerçek Dünya Deneyimi

5+ yıllık production deneyimine dayalı kapsamlı AWS Lambda rehberi. Cold start optimizasyonu, performans ayarlama, monitoring ve maliyet optimizasyonu ile gerçek savaş hikayeleri ve pratik çözümler.

İlerleme 3/4 yazı tamamlandı

İlgili yazılar