EngineeringSenior DeveloperTechnical Sessions

Session 18: API Gateway, Service Mesh & Observability

Mục tiêu: Thiết kế API Gateway, hiểu Service Mesh, và implement distributed observability.


1. API Gateway

YARP (Yet Another Reverse Proxy) - Microsoft

csharp
// .NET based reverse proxy, built by Microsoft
// Install: Yarp.ReverseProxy

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

app.MapReverseProxy();
json
// appsettings.json
{
  "ReverseProxy": {
    "Routes": {
      "orders-route": {
        "ClusterId": "orders-cluster",
        "Match": { "Path": "/api/orders/{**catch-all}" },
        "Transforms": [
          { "PathRemovePrefix": "/api" }
        ]
      },
      "catalog-route": {
        "ClusterId": "catalog-cluster",
        "Match": { "Path": "/api/products/{**catch-all}" },
        "Transforms": [
          { "PathRemovePrefix": "/api" }
        ]
      }
    },
    "Clusters": {
      "orders-cluster": {
        "Destinations": {
          "orders-service": { "Address": "https://orders-service:8080" }
        },
        "HealthCheck": {
          "Active": {
            "Enabled": true,
            "Interval": "00:00:30",
            "Path": "/health"
          }
        }
      },
      "catalog-cluster": {
        "LoadBalancingPolicy": "RoundRobin",
        "Destinations": {
          "catalog-1": { "Address": "https://catalog-1:8080" },
          "catalog-2": { "Address": "https://catalog-2:8080" }
        }
      }
    }
  }
}

Gateway Patterns

csharp
// 1. Rate Limiting tại Gateway
app.MapReverseProxy(proxyPipeline =>
{
    proxyPipeline.UseRateLimiter();
    proxyPipeline.UseAuthentication();
    proxyPipeline.UseAuthorization();
});

// 2. API Composition / BFF (Backend for Frontend)
app.MapGet("/api/dashboard", async (
    HttpClient ordersClient,
    HttpClient catalogClient,
    HttpClient analyticsClient) =>
{
    // Aggregate data từ nhiều services
    var ordersTask = ordersClient.GetFromJsonAsync<OrderSummary[]>("/orders/recent");
    var catalogTask = catalogClient.GetFromJsonAsync<ProductSummary[]>("/products/top");
    var analyticsTask = analyticsClient.GetFromJsonAsync<AnalyticsData>("/stats/today");

    await Task.WhenAll(ordersTask, catalogTask, analyticsTask);

    return new DashboardResponse
    {
        RecentOrders = ordersTask.Result!,
        TopProducts = catalogTask.Result!,
        TodayStats = analyticsTask.Result!
    };
});

2. Service Mesh - Dapr

┌────────────────────────────────────────┐
│           Dapr Sidecar Pattern          │
│                                         │
│  ┌───────────┐    ┌──────────────────┐ │
│  │ Your App  │←──→│   Dapr Sidecar   │ │
│  │ (.NET)    │    │   (localhost)     │ │
│  │           │    │                  │ │
│  │ HTTP/gRPC │    │ - Service invoke │ │
│  │ :8080     │    │ - State store    │ │
│  │           │    │ - Pub/Sub        │ │
│  └───────────┘    │ - Secrets        │ │
│                   │ - Observability  │ │
│                   └──────────────────┘ │
└────────────────────────────────────────┘
csharp
// Dapr .NET SDK
builder.Services.AddDaprClient();

// Service invocation (thay HTTP/gRPC direct)
app.MapGet("/api/orders", async (DaprClient dapr) =>
{
    // Call "catalog-service" qua Dapr sidecar
    var products = await dapr.InvokeMethodAsync<List<ProductDto>>(
        HttpMethod.Get, "catalog-service", "api/products");

    return products;
});

// State management
await daprClient.SaveStateAsync("statestore", "order:123", orderData);
var order = await daprClient.GetStateAsync<OrderData>("statestore", "order:123");

// Pub/Sub
await daprClient.PublishEventAsync("pubsub", "orders", new OrderCreated { ... });

3. Distributed Tracing - OpenTelemetry

csharp
// Setup OpenTelemetry cho .NET
builder.Services.AddOpenTelemetry()
    .ConfigureResource(resource => resource
        .AddService("order-service", serviceVersion: "1.0.0"))
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddSource("MassTransit")
        .AddOtlpExporter(options =>
        {
            options.Endpoint = new Uri("http://jaeger:4317");
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddProcessInstrumentation()
        .AddOtlpExporter());

// Custom Activity (Span)
public class OrderService
{
    private static readonly ActivitySource _activitySource = new("OrderService");

    public async Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
    {
        using var activity = _activitySource.StartActivity("CreateOrder");
        activity?.SetTag("customer.id", cmd.CustomerId);
        activity?.SetTag("items.count", cmd.Items.Count);

        try
        {
            var order = Order.Create(cmd.CustomerId, cmd.Address);
            activity?.SetTag("order.id", order.Id);

            // Child span cho database operation
            using var dbActivity = _activitySource.StartActivity("SaveToDatabase");
            await _repository.AddAsync(order);

            activity?.SetStatus(ActivityStatusCode.Ok);
            return order;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            activity?.RecordException(ex);
            throw;
        }
    }
}

Tracing Visualization

Trace: create-order (total: 250ms)
│
├── [order-service] POST /api/orders (250ms)
│   ├── [order-service] ValidateCommand (5ms)
│   ├── [catalog-service] GET /products/check-stock (45ms)
│   │   └── [catalog-service] DB Query (12ms)
│   ├── [order-service] CreateOrder (10ms)
│   ├── [order-service] SaveToDatabase (30ms)
│   └── [order-service] PublishEvent (15ms)
│       └── [rabbitmq] Publish OrderSubmitted (8ms)
│
└── [shipping-service] Handle OrderSubmitted (120ms)  ← async
    ├── [shipping-service] CreateShipment (20ms)
    └── [shipping-service] SaveToDatabase (15ms)

4. Centralized Logging - Serilog

csharp
// Serilog + Seq/ELK
builder.Host.UseSerilog((context, config) =>
{
    config
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.WithProperty("ServiceName", "order-service")
        .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
        .Enrich.WithCorrelationId()
        .Enrich.FromLogContext()
        .WriteTo.Console(new CompactJsonFormatter())
        .WriteTo.Seq("http://seq:5341")  // Seq server
        // .WriteTo.Elasticsearch(...)    // ELK stack
        ;
});

// Structured logging
logger.LogInformation(
    "Order {OrderId} created for customer {CustomerId} with {ItemCount} items, total {Total}",
    order.Id, order.CustomerId, order.Items.Count, order.Total);

// → JSON output:
// {
//   "Timestamp": "2024-01-15T10:30:00Z",
//   "Level": "Information",
//   "MessageTemplate": "Order {OrderId} created...",
//   "Properties": {
//     "OrderId": "abc-123",
//     "CustomerId": "cust-456",
//     "ItemCount": 3,
//     "Total": 150.00,
//     "ServiceName": "order-service",
//     "CorrelationId": "corr-789"
//   }
// }

// Correlation ID middleware
public class CorrelationIdMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers["X-Correlation-Id"].FirstOrDefault()
            ?? Guid.NewGuid().ToString();

        context.Response.Headers["X-Correlation-Id"] = correlationId;

        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            await next(context);
        }
    }
}

5. Metrics & Alerting

csharp
// Custom metrics
public class OrderMetrics
{
    private static readonly Counter<long> _ordersCreated =
        Meter.CreateCounter<long>("orders.created", "orders",
            "Total orders created");

    private static readonly Histogram<double> _orderProcessingTime =
        Meter.CreateHistogram<double>("orders.processing_time", "ms",
            "Order processing duration");

    private static readonly UpDownCounter<int> _activeOrders =
        Meter.CreateUpDownCounter<int>("orders.active", "orders",
            "Currently active orders");

    private static readonly Meter Meter = new("OrderService", "1.0.0");

    public void OrderCreated(string category)
    {
        _ordersCreated.Add(1, new KeyValuePair<string, object?>("category", category));
    }

    public IDisposable MeasureProcessingTime()
    {
        var sw = Stopwatch.StartNew();
        return new Timer(() => _orderProcessingTime.Record(sw.ElapsedMilliseconds));
    }
}

6. Production Monitoring Setup

yaml
# docker-compose.monitoring.yml
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686"    # Jaeger UI
      - "4317:4317"      # OTLP gRPC
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  seq:
    image: datalust/seq:latest
    ports:
      - "5341:5341"      # Ingestion
      - "8081:80"        # UI
    environment:
      - ACCEPT_EULA=Y

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin

Tổng Kết Session 18

ConceptKey Takeaway
YARP.NET reverse proxy, routing, load balancing, health checks
DaprSidecar pattern, abstracts infrastructure
OpenTelemetryDistributed tracing, metrics, cross-service correlation
Serilog + SeqStructured logging, centralized search
Correlation IDTrack request across services
MetricsCustom counters, histograms cho business metrics

Bài tập: Setup YARP gateway routing đến 2 backend services. Add OpenTelemetry tracing + Serilog structured logging. Deploy Jaeger + Seq bằng Docker Compose.