Skip to content

Latest commit

 

History

History
1140 lines (940 loc) · 30.4 KB

File metadata and controls

1140 lines (940 loc) · 30.4 KB

Dickson's Developer Assessment - Part 2

Performance & Scalability

12. How would you reduce unnecessary API calls?

Caching Strategy:

// CacheService.php
class CacheService {
    private $redis;
    
    public function __construct() {
        if (class_exists('Redis')) {
            $this->redis = new Redis();
            $this->redis->connect('127.0.0.1', 6379);
        }
    }
    
    public function get($key) {
        if ($this->redis) {
            $data = $this->redis->get($key);
            return $data ? json_decode($data, true) : null;
        }
        
        // Fallback to file cache
        $file = sys_get_temp_dir() . '/' . md5($key) . '.cache';
        if (file_exists($file) && (time() - filemtime($file)) < 300) {
            return json_decode(file_get_contents($file), true);
        }
        
        return null;
    }
    
    public function set($key, $value, $ttl = 300) {
        if ($this->redis) {
            $this->redis->setex($key, $ttl, json_encode($value));
        } else {
            // File cache fallback
            $file = sys_get_temp_dir() . '/' . md5($key) . '.cache';
            file_put_contents($file, json_encode($value));
        }
    }
}

// In api/track.php
$cache = new CacheService();
$cacheKey = "tracking:{$trackingNumber}";

// Check cache first
if ($cached = $cache->get($cacheKey)) {
    sendResponse(true, $cached, 'Retrieved from cache');
}

// If not cached, call API
$apiData = makeApiRequest(...);

// Cache for 5 minutes
$cache->set($cacheKey, $trackingInfo, 300);

Request Deduplication:

// RequestDeduplicator.php
class RequestDeduplicator {
    private static $pendingRequests = [];
    
    public static function execute($key, callable $callback) {
        // If request is already pending, wait for it
        if (isset(self::$pendingRequests[$key])) {
            while (isset(self::$pendingRequests[$key])) {
                usleep(100000); // Wait 100ms
            }
            // Return cached result
            return (new CacheService())->get($key);
        }
        
        // Mark as pending
        self::$pendingRequests[$key] = true;
        
        try {
            $result = $callback();
            return $result;
        } finally {
            unset(self::$pendingRequests[$key]);
        }
    }
}

// Usage
$result = RequestDeduplicator::execute("tracking:{$trackingNumber}", function() {
    return makeApiRequest(...);
});

Client-Side Caching:

// In track.js
const trackingCache = new Map();

function trackParcel(trackingNumber) {
    // Check local cache (5 minutes)
    const cached = trackingCache.get(trackingNumber);
    if (cached && (Date.now() - cached.timestamp) < 300000) {
        displayTrackingResults(cached.data);
        return;
    }
    
    // Make request
    fetch('api/track.php', {...})
        .then(data => {
            // Cache result
            trackingCache.set(trackingNumber, {
                data: data.data,
                timestamp: Date.now()
            });
            displayTrackingResults(data.data);
        });
}

Batch Requests:

// For tracking multiple parcels
function trackMultiple(array $trackingNumbers): array {
    // Group into single API call if provider supports it
    $results = [];
    
    foreach (array_chunk($trackingNumbers, 10) as $batch) {
        $response = makeApiRequest('/tracktrace/batch', [
            'tracking_numbers' => implode(',', $batch)
        ]);
        $results = array_merge($results, $response);
    }
    
    return $results;
}

13. What caching strategies could be applied?

Multi-Layer Caching Architecture:

Client Browser Cache (60 seconds)
    ↓
CDN Cache (5 minutes) - Static assets
    ↓
Application Cache - Redis (5 minutes)
    ↓
Database Query Cache (if using DB)
    ↓
API Response Cache (5 minutes)
    ↓
External API (Fastway)

Implementation:

// 1. HTTP Caching Headers
header('Cache-Control: public, max-age=60');
header('ETag: ' . md5($trackingNumber . $timestamp));

if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && 
    $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
    http_response_code(304);
    exit;
}

// 2. Redis Cache
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$cacheKey = "tracking:{$trackingNumber}";
$ttl = 300; // 5 minutes

if ($cached = $redis->get($cacheKey)) {
    return json_decode($cached, true);
}

$data = callApi();
$redis->setex($cacheKey, $ttl, json_encode($data));

// 3. Memcached (Alternative)
$memcached = new Memcached();
$memcached->addServer('localhost', 11211);

$data = $memcached->get($cacheKey);
if ($data === false) {
    $data = callApi();
    $memcached->set($cacheKey, $data, 300);
}

// 4. APCu (In-Memory Cache)
$cacheKey = "tracking_{$trackingNumber}";
$data = apcu_fetch($cacheKey, $success);

if (!$success) {
    $data = callApi();
    apcu_store($cacheKey, $data, 300);
}

// 5. File-Based Cache (Fallback)
$cacheFile = __DIR__ . "/cache/" . md5($trackingNumber) . ".json";
$cacheAge = file_exists($cacheFile) ? time() - filemtime($cacheFile) : 999999;

if ($cacheAge < 300) {
    $data = json_decode(file_get_contents($cacheFile), true);
} else {
    $data = callApi();
    file_put_contents($cacheFile, json_encode($data));
}

Cache Invalidation Strategies:

// 1. Time-Based (TTL)
$cache->setex($key, 300, $value); // Expires after 5 minutes

// 2. Event-Based
class TrackingEventListener {
    public function onTrackingUpdated($trackingNumber) {
        $cache->delete("tracking:{$trackingNumber}");
    }
}

// 3. Tag-Based
$cache->tags(['tracking', 'active'])->put($key, $value, 300);
$cache->tags(['tracking'])->flush(); // Clear all tracking cache

// 4. LRU (Least Recently Used)
$redis->config('SET', 'maxmemory-policy', 'allkeys-lru');

14. How would your solution scale with 100 vs 10,000 users per day?

100 Users/Day (Current Architecture - Sufficient):

- Single PHP server (Google App Engine F1 instance)
- File-based caching acceptable
- No database needed
- ~4 requests per hour
- Cost: $0-10/month

10,000 Users/Day (Scaled Architecture):

Architecture:
┌─────────────┐
│  Cloud CDN  │ ← Static assets (CSS, JS, images)
└──────┬──────┘
       ↓
┌─────────────┐
│Load Balancer│ ← Distributes traffic
└──────┬──────┘
       ↓
┌──────┴──────┬──────────┬──────────┐
│   App       │   App    │   App    │
│ Instance 1  │Instance 2│Instance 3│ ← Auto-scaling
└──────┬──────┴────┬─────┴────┬─────┘
       ↓           ↓           ↓
┌─────────────────────────────────┐
│      Redis Cluster              │ ← Shared cache
└─────────────────────────────────┘
       ↓           ↓           ↓
┌──────────────────────────────────┐
│   Cloud SQL / PostgreSQL         │ ← Database (if needed)
└──────────────────────────────────┘
       ↓
┌──────────────────────────────────┐
│   Cloud Logging & Monitoring     │
└──────────────────────────────────┘

Google App Engine Configuration:

# app.yaml for high traffic
runtime: php81
env: standard

automatic_scaling:
  min_instances: 2        # Always-on instances
  max_instances: 20       # Scale up to 20
  target_cpu_utilization: 0.6
  target_throughput_utilization: 0.7

instance_class: F2       # More memory (512MB)

# Session affinity for cache consistency
session_affinity: true

vpc_access_connector:
  name: projects/PROJECT/locations/REGION/connectors/redis-connector

env_variables:
  REDIS_HOST: '10.0.0.3'
  REDIS_PORT: '6379'

Performance Optimizations:

// 1. Connection Pooling
class DatabasePool {
    private static $connections = [];
    
    public static function getConnection() {
        if (empty(self::$connections)) {
            self::$connections[] = new PDO(...);
        }
        return self::$connections[0];
    }
}

// 2. Async API Calls
use React\EventLoop\Factory;
use React\HttpClient\Client;

$loop = Factory::create();
$client = new Client($loop);

$requests = [];
foreach ($trackingNumbers as $number) {
    $requests[] = $client->request('GET', "/api/track/{$number}");
}

Promise\all($requests)->then(function($results) {
    // Process all results
});

$loop->run();

// 3. Database Query Optimization
// Use prepaed statements with query caching
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

// Add indexes
CREATE INDEX idx_tracking_number ON tracking (tracking_number);
CREATE INDEX idx_created_at ON tracking (created_at);

// 4. OpCode Caching
// Enable OPcache in php.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000

Cost Comparison:

Users/Day Requests/Hour Instances Cost/Month
100 4 1 (F1) $5
1,000 42 1-2 (F1) $20
10,000 417 2-5 (F2) $150
100,000 4,167 10-20 (F4) $1,500

15. How could this system be converted into a microservice-friendly architecture?

Microservices Architecture:

┌──────────────────────────────────────────────────────┐
│                 API Gateway (Kong/Nginx)              │
│              Route, Auth, Rate Limit                  │
└────┬──────────┬──────────┬──────────┬────────────────┘
     │          │          │          │
     ↓          ↓          ↓          ↓
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│Track   │ │Quote   │ │User    │ │Notif   │
│Service │ │Service │ │Service │ │Service │
└────┬───┘ └────┬───┘ └────┬───┘ └────┬───┘
     │          │          │          │
     └──────────┴──────────┴──────────┘
                    ↓
           ┌─────────────────┐
           │  Message Queue  │ (RabbitMQ/Pub/Sub)
           └─────────────────┘
                    ↓
           ┌─────────────────┐
           │ Shared Services │
           │ - Cache (Redis) │
           │ - Database      │
           │ - Logging       │
           └─────────────────┘

Implementation:

fastway-microservices/
├── services/
│   ├── tracking-service/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   ├── composer.json
│   │   └── kubernetes/
│   │       └── deployment.yaml
│   ├── quote-service/
│   │   ├── src/
│   │   ├── Dockerfile
│   │   └── kubernetes/
│   ├── notification-service/
│   │   ├── src/
│   │   └── Dockerfile
│   └── user-service/
│       ├── src/
│       └── Dockerfile
├── api-gateway/
│   ├── kong.yml
│   └── nginx.conf
├── shared/
│   ├── proto/          # gRPC definitions
│   └── libraries/      # Shared code
└── docker-compose.yml

Tracking Microservice (Standalone):

// tracking-service/src/index.php
<?php
require 'vendor/autoload.php';

use FastwayTracking\TrackingController;
use FastwayTracking\FastwayAPIClient;
use FastwayTracking\CacheService;

$app = new \Slim\App();

// Health check
$app->get('/health', function($request, $response) {
    return $response->withJson(['status' => 'healthy']);
});

// Track endpoint
$app->post('/track', function($request, $response) {
    $data = $request->getParsedBody();
    $trackingNumber = $data['tracking_number'];
    
    $cache = new CacheService();
    $client = new FastwayAPIClient();
    $controller = new TrackingController($client, $cache);
    
    try {
        $result = $controller->track($trackingNumber);
        return $response->withJson(['success' => true, 'data' => $result]);
    } catch (\Exception $e) {
        return $response->withStatus(500)
                       ->withJson(['error' => $e->getMessage()]);
    }
});

$app->run();

Dockerfile:

# tracking-service/Dockerfile
FROM php:8.1-fpm

# Install dependencies
RUN apt-get update && apt-get install -y \
    libzip-dev \
    && docker-php-ext-install zip pdo_mysql

# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

# Copy application
WORKDIR /app
COPY . /app

RUN composer install --no-dev --optimize-autoloader

EXPOSE 8080

CMD ["php", "-S", "0.0.0.0:8080", "-t", "src"]

Kubernetes Deployment:

# tracking-service/kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tracking-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: tracking-service
  template:
    metadata:
      labels:
        app: tracking-service
    spec:
      containers:
      - name: tracking-service
        image: gcr.io/PROJECT/tracking-service:latest
        ports:
        - containerPort: 8080
        env:
        - name: REDIS_HOST
          value: "redis-service"
        - name: FASTWAY_API_KEY
          valueFrom:
            secretKeyRef:
              name: fastway-secrets
              key: api-key
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: tracking-service
spec:
  selector:
    app: tracking-service
  ports:
  - port: 80
    targetPort: 8080
  type: LoadBalancer

Inter-Service Communication (gRPC):

// shared/proto/tracking.proto
syntax = "proto3";

package tracking;

service TrackingService {
    rpc Track(TrackRequest) returns (TrackResponse);
}

message TrackRequest {
    string tracking_number = 1;
}

message TrackResponse {
    string tracking_number = 1;
    string status = 2;
    repeated Event events = 3;
}

message Event {
    string date = 1;
    string description = 2;
    string location = 3;
}

Message Queue for Async Processing:

// notification-service/src/NotificationWorker.php
use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection('rabbitmq', 5672, 'guest', 'guest');
$channel = $connection->channel();

$channel->queue_declare('tracking_notifications', false, true, false, false);

$callback = function($msg) {
    $data = json_decode($msg->body, true);
    
    // Send notification
    sendEmail($data['email'], "Tracking Update", $data['message']);
    
    $msg->ack();
};

$channel->basic_consume('tracking_notifications', '', false, false, false, false, $callback);

while ($channel->is_consuming()) {
    $channel->wait();
}

Benefits of Microservices:

  • Independent scaling of services
  • Technology flexibility (use Node.js for real-time, Python for ML)
  • Fault isolation
  • Independent deployment
  • Team autonomy

Error Handling & Logging

16. How did you implement error handling in your application?

Multi-Layer Error Handling:

// 1. Global Exception Handler
set_exception_handler(function($exception) {
    logError('Uncaught Exception', [
        'message' => $exception->getMessage(),
        'file' => $exception->getFile(),
        'line' => $exception->getLine(),
        'trace' => $exception->getTraceAsString()
    ]);
    
    http_response_code(500);
    echo json_encode([
        'success' => false,
        'error' => 'An unexpected error occurred'
    ]);
});

// 2. Custom Error Handler
set_error_handler(function($errno, $errstr, $errfile, $errline) {
    if (!(error_reporting() & $errno)) {
        return false;
    }
    
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

// 3. Try-Catch in Critical Sections
try {
    $response = curl_exec($ch);
    
    if ($curlError = curl_error($ch)) {
        throw new APIException("cURL Error: $curlError");
    }
    
    if ($httpCode !== 200) {
        throw new HTTPException("HTTP $httpCode received");
    }
    
    $data = json_decode($response, true);
    
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new JSONException("JSON parse error: " . json_last_error_msg());
    }
    
} catch (APIException $e) {
    logError('API Error', ['error' => $e->getMessage()]);
    sendResponse(false, [], 'Unable to connect to tracking service');
    
} catch (JSONException $e) {
    logError('Data Format Error', ['error' => $e->getMessage()]);
    sendResponse(false, [], 'Invalid response from tracking service');
    
} catch (Exception $e) {
    logError('General Error', ['error' => $e->getMessage()]);
    sendResponse(false, [], 'An error occurred processing your request');
}

// 4. Input Validation Errors
if (!validateInput($trackingNumber)) {
    http_response_code(400);
    sendResponse(false, [], 'Invalid tracking number format');
}

// 5. Business Logic Errors
if ($weight > 30) {
    sendResponse(false, [], 'Maximum weight is 30kg');
}

Custom Exception Classes:

// exceptions/APIException.php
class APIException extends Exception {
    protected $apiResponse;
    
    public function __construct($message, $apiResponse = null) {
        parent::__construct($message);
        $this->apiResponse = $apiResponse;
    }
    
    public function getAPIResponse() {
        return $this->apiResponse;
    }
}

// exceptions/ValidationException.php
class ValidationException extends Exception {
    protected $errors;
    
    public function __construct($message, array $errors = []) {
        parent::__construct($message);
        $this->errors = $errors;
    }
    
    public function getErrors() {
        return $this->errors;
    }
}

17. How would you implement centralized error logging?

Centralized Logging Architecture:

// Logger.php  OR // in this case you can check the /logs/error.log as well
class Logger {
    private static $instance;
    private $handlers = [];
    
    private function __construct() {
        // Add handlers
        $this->addHandler(new FileHandler());
        $this->addHandler(new CloudLoggingHandler());
        $this->addHandler(new SentryHandler());
    }
    
    public static function getInstance() {
        if (!self::$instance) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    public function log($level, $message, array $context = []) {
        $entry = [
            'timestamp' => date('c'),
            'level' => $level,
            'message' => $message,
            'context' => $context,
            'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? uniqid(),
            'user_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
        ];
        
        foreach ($this->handlers as $handler) {
            $handler->handle($entry);
        }
    }
    
    public function error($message, array $context = []) {
        $this->log('ERROR', $message, $context);
    }
    
    public function warning($message, array $context = []) {
        $this->log('WARNING', $message, $context);
    }
    
    public function info($message, array $context = []) {
        $this->log('INFO', $message, $context);
    }
}

// Handlers/FileHandler.php
class FileHandler {
    public function handle(array $entry) {
        $logFile = __DIR__ . '/../logs/' . date('Y-m-d') . '.log';
        $line = sprintf(
            "[%s] %s: %s %s\n",
            $entry['timestamp'],
            $entry['level'],
            $entry['message'],
            json_encode($entry['context'])
        );
        file_put_contents($logFile, $line, FILE_APPEND);
    }
}

// Handlers/CloudLoggingHandler.php
class CloudLoggingHandler {
    private $client;
    
    public function __construct() {
        $this->client = new \Google\Cloud\Logging\LoggingClient();
    }
    
    public function handle(array $entry) {
        $logger = $this->client->logger('fastway-app');
        $logger->write($entry, [
            'severity' => $entry['level']
        ]);
    }
}


// Usage
$logger = Logger::getInstance();
$logger->error('API call failed', [
    'tracking_number' => $trackingNumber,
    'error' => $errorMessage,
    'api_response' => $apiResponse
]);

Structured Logging Format:

{
  "timestamp": "2026-01-24T15:30:00+00:00",
  "level": "ERROR",
  "message": "Fastway API call failed",
  "context": {
    "tracking_number": "Z60000983328",
    "http_code": 500,
    "error": "Connection timeout"
  },
  "request_id": "req_abc123",
  "user_ip": "192.168.1.1",
  "user_agent": "Mozilla/5.0...",
  "trace_id": "trace_xyz789",
  "span_id": "span_456"
}

18. How would you monitor API failures in a production environment?

Monitoring Stack:

// Monitoring/MetricsCollector.php
class MetricsCollector {
    private $statsd;
    
    public function __construct() {
        $this->statsd = new \Domnikl\Statsd\Client(
            new \Domnikl\Statsd\Connection\UdpSocket('statsd', 8125)
        );
    }
    
    public function incrementAPICall($endpoint, $success) {
        $metric = $success ? 'api.success' : 'api.failure';
        $this->statsd->increment($metric, 1, ['endpoint' => $endpoint]);
    }
    
    public function recordAPILatency($endpoint, $duration) {
        $this->statsd->timing('api.latency', $duration, ['endpoint' => $endpoint]);
    }
    
    public function gaugeActiveRequests($count) {
        $this->statsd->gauge('api.active_requests', $count);
    }
}

// In api/track.php
$metrics = new MetricsCollector();
$startTime = microtime(true);

try {
    $response = makeApiRequest(...);
    $metrics->incrementAPICall('track', true);
} catch (Exception $e) {
    $metrics->incrementAPICall('track', false);
    throw $e;
} finally {
    $duration = (microtime(true) - $startTime) * 1000; // ms
    $metrics->recordAPILatency('track', $duration);
}

Health Check Endpoint:

// health-check.php  // You can visit https://fastway-webapp.ue.r.appspot.com/health-check.php as well if you are that lazy LOL //
class HealthChecker {
    public function check(): array {
        return [
            'status' => $this->getOverallStatus(),
            'checks' => [
                'api' => $this->checkAPIAvailability(),
                'database' => $this->checkDatabase(),
                'redis' => $this->checkRedis(),
                'disk' => $this->checkDiskSpace(),
                'memory' => $this->checkMemory()
            ],
            'timestamp' => date('c')
        ];
    }
    
    private function checkAPIAvailability(): array {
        try {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, 'https://sa.api.fastway.org/v3/health');
            curl_setopt($ch, CURLOPT_TIMEOUT, 5);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            
            return [
                'status' => $httpCode === 200 ? 'healthy' : 'unhealthy',
                'response_code' => $httpCode
            ];
        } catch (Exception $e) {
            return ['status' => 'unhealthy', 'error' => $e->getMessage()];
        }
    }
}

Monitoring Dashboard (Grafana):

# prometheus.yml  // learned this from BMW, they are also trying to build a tech flow for the SA market //
scrape_configs:
  - job_name: 'fastway-app'
    static_configs:
      - targets: ['app:8080']
    metrics_path: '/metrics'
    scrape_interval: 15s

# Metrics exposed
# HELP api_requests_total Total number of API requests
# TYPE api_requests_total counter
api_requests_total{endpoint="track",status="success"} 1523
api_requests_total{endpoint="track",status="failure"} 12

# HELP api_latency_seconds API request latency
# TYPE api_latency_seconds histogram
api_latency_seconds_bucket{endpoint="track",le="0.1"} 1200
api_latency_seconds_bucket{endpoint="track",le="0.5"} 1500
api_latency_seconds_bucket{endpoint="track",le="1.0"} 1523

19. How would you alert administrators if the Fastway API was unavailable?

Alert Management System:

// Alerting/AlertManager.php
class AlertManager {
    private $channels;
    
    public function __construct() {
        $this->channels = [
            new EmailChannel(),
            new SlackChannel(),
            new PagerDutyChannel(),
            new SMSChannel()
        ];
    }
    
    public function sendAlert($severity, $message, array $context = []) {
        $alert = [
            'severity' => $severity,
            'message' => $message,
            'context' => $context,
            'timestamp' => date('c'),
            'environment' => getenv('APP_ENV')
        ];
        
        // Send based on severity
        foreach ($this->getChannelsForSeverity($severity) as $channel) {
            $channel->send($alert);
        }
    }
    
    private function getChannelsForSeverity($severity) {
        switch ($severity) {
            case 'CRITICAL':
                return $this->channels; // All channels
            case 'WARNING':
                return [new EmailChannel(), new SlackChannel()];
            case 'INFO':
                return [new SlackChannel()];
            default:
                return [];
        }
    }
}

// Channels/SlackChannel.php
class SlackChannel {
    public function send(array $alert) {
        $webhook = getenv('SLACK_WEBHOOK_URL');
        
        $payload = [
            'text' => $alert['message'],
            'attachments' => [[
                'color' => $this->getColor($alert['severity']),
                'fields' => [
                    ['title' => 'Severity', 'value' => $alert['severity'], 'short' => true],
                    ['title' => 'Environment', 'value' => $alert['environment'], 'short' => true],
                    ['title' => 'Details', 'value' => json_encode($alert['context'])]
                ],
                'footer' => 'Fastway App',
                'ts' => strtotime($alert['timestamp'])
            ]]
        ];
        
        $ch = curl_init($webhook);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
        curl_exec($ch);
        curl_close($ch);
    }
}

// Channels/PagerDutyChannel.php
class PagerDutyChannel {
    public function send(array $alert) {
        if ($alert['severity'] !== 'CRITICAL') {
            return; // Only page for critical issues
        }
        
        $payload = [
            'routing_key' => getenv('PAGERDUTY_KEY'),
            'event_action' => 'trigger',
            'payload' => [
                'summary' => $alert['message'],
                'severity' => 'critical',
                'source' => 'fastway-app',
                'custom_details' => $alert['context']
            ]
        ];
        
        $ch = curl_init('https://events.pagerduty.com/v2/enqueue');
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
        curl_exec($ch);
        curl_close($ch);
    }
}

API Health Monitoring:

// Cron job or scheduled task
// cron/check-api-health.php
class APIHealthMonitor {
    private $alertManager;
    private $failureCount = 0;
    private $maxFailures = 3;
    
    public function __construct() {
        $this->alertManager = new AlertManager();
    }
    
    public function monitor() {
        $isHealthy = $this->checkAPIHealth();
        
        if (!$isHealthy) {
            $this->failureCount++;
            
            if ($this->failureCount >= $this->maxFailures) {
                $this->alertManager->sendAlert('CRITICAL', 
                    'Fastway API is unavailable', 
                    [
                        'consecutive_failures' => $this->failureCount,
                        'last_check' => date('c')
                    ]
                );
            }
        } else {
            if ($this->failureCount > 0) {
                // API recovered
                $this->alertManager->sendAlert('INFO',
                    'Fastway API has recovered',
                    ['downtime_checks' => $this->failureCount]
                );
            }
            $this->failureCount = 0;
        }
    }
    
    private function checkAPIHealth(): bool {
        try {
            $response = file_get_contents(
                'https://sa.api.fastway.org/v3/health?api_key=' . FASTWAY_API_KEY,
                false,
                stream_context_create(['http' => ['timeout' => 5]])
            );
            return !empty($response);
        } catch (Exception $e) {
            return false;
        }
    }
}

// Run every 5 minutes
$monitor = new APIHealthMonitor();
$monitor->monitor();

Alerting Rules (Prometheus):

# alert.rules.yml
groups:
- name: fastway-api
  interval: 30s
  rules:
  - alert: FastwayAPIDown
    expr: api_requests_total{status="failure"} / api_requests_total > 0.5
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "Fastway API failure rate above 50%"
      description: "{{ $value }}% of requests are failing"
      
  - alert: FastwayAPIHighLatency
    expr: histogram_quantile(0.95, api_latency_seconds) > 2
    for: 10m
    labels:
      severity: warning
    annotations:
      summary: "Fastway API latency high"
      description: "95th percentile latency is {{ $value }}s"

Continue in next message with Testing Strategy...