Estándares de Plataforma
Variables de Entorno
Categorías
| Categoría | Ejemplo | En código |
|---|---|---|
| URLs de servicios | DATABASE_WRITE_URL, RABBITMQ_URL | Requerida (fail-fast) |
| Secrets | CLIENT_SECRET, HMAC_SECRET | Requerida (fail-fast) |
| Configuración operacional | TENANT_CACHE_TTL_SECONDS | Con default |
| Feature flags | ENABLE_MAKER_CHECKER | Con default (false) |
Naming de Variables
{SERVICE}_{COMPONENT}_{PROPERTY}
Ejemplos:
DATABASE_WRITE_URL
DATABASE_READ_URL
RABBITMQ_URL
RABBITMQ_USER
RABBITMQ_PASSWORD
KEYCLOAK_URL
KEYCLOAK_CLIENT_ID
KEYCLOAK_CLIENT_SECRET
REDIS_URL
REDIS_KEY_PREFIXValidación al Startup (Fail-Fast)
csharp
public class EnvConfig
{
public string DatabaseWriteUrl { get; } = Required("DATABASE_WRITE_URL");
public string DatabaseReadUrl { get; } = Required("DATABASE_READ_URL");
public string RabbitMqUrl { get; } = Required("RABBITMQ_URL");
public int TenantCacheTtl { get; } = Optional("TENANT_CACHE_TTL_SECONDS", 300);
private static string Required(string name) =>
Environment.GetEnvironmentVariable(name)
?? throw new InvalidOperationException($"Missing required env var: {name}");
}Health Checks
Cada servicio expone:
| Endpoint | Propósito | Auth |
|---|---|---|
/health | Health check completo (BD + Redis + RabbitMQ) | Ninguna |
/health/ready | Readiness (puede recibir tráfico) | Ninguna |
/health/live | Liveness (proceso vivo) | Ninguna |
csharp
builder.Services.AddHealthChecks()
.AddNpgSql(config.DatabaseWriteUrl, name: "postgresql")
.AddRedis(config.RedisUrl, name: "redis")
.AddRabbitMQ(config.RabbitMqUrl, name: "rabbitmq");Logging
Formato
Structured logging con Serilog. Output JSON en producción, console en desarrollo.
csharp
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Service", "imagy-lending")
.Enrich.WithProperty("Environment", env)
.WriteTo.Console(new JsonFormatter()) // producción
.CreateLogger();Campos obligatorios en cada log
| Campo | Fuente | Ejemplo |
|---|---|---|
Service | Configuración | imagy-lending |
RequestId | Header o generado | a1b2c3d4 |
TenantId | IIdentityContext | uuid |
UserId | IIdentityContext | uuid |
CorrelationId | Header o evento | uuid |
Niveles
| Nivel | Cuándo |
|---|---|
Error | Excepciones no manejadas, fallos de integración |
Warning | Situaciones inesperadas pero recuperables |
Information | Operaciones de negocio completadas |
Debug | Detalles técnicos (solo en desarrollo) |
Nunca loguear
- Passwords o secrets
- Tokens JWT completos
- Datos biométricos
- Números de documento completos (solo últimos 4 dígitos)
Docker
Multi-stage Build (patrón estándar)
dockerfile
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app .
USER app
EXPOSE 8080
ENTRYPOINT ["dotnet", "Imagy.{Domain}.Api.dll"]Reglas
- Base image: siempre Alpine (mínima superficie de ataque)
- Non-root user: siempre
USER app - Puerto: 8080 (estándar ECS Fargate)
- No secrets en la imagen
.dockerignoreactualizado
Docker Compose (desarrollo local)
Cada servicio tiene su docker-compose.yml para desarrollo local. Además existe un docker-compose.infra.yml compartido para la infraestructura:
yaml
# docker-compose.infra.yml (en imagy-infra o en cada repo)
services:
postgres:
image: postgres:16-alpine
ports: ["5432:5432"]
environment:
POSTGRES_PASSWORD: localdev
volumes:
- pgdata:/var/lib/postgresql/data
valkey:
image: valkey/valkey:8-alpine
ports: ["6379:6379"]
rabbitmq:
image: rabbitmq:3-management-alpine
ports: ["5672:5672", "15672:15672"]
keycloak:
image: quay.io/keycloak/keycloak:24.0
ports: ["8080:8080"]
command: start-dev
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
volumes:
pgdata:Respuestas API (Envelope estándar)
Éxito
json
{
"data": { ... },
"metadata": {
"request_id": "a1b2c3d4",
"timestamp": "2026-05-18T10:30:00Z"
}
}Éxito paginado
json
{
"data": [ ... ],
"metadata": {
"request_id": "a1b2c3d4",
"timestamp": "2026-05-18T10:30:00Z",
"pagination": {
"page": 1,
"page_size": 20,
"total": 150,
"total_pages": 8
}
}
}Error
json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more validation errors occurred",
"details": [
{ "field": "amount", "message": "Must be greater than 0" }
]
},
"metadata": {
"request_id": "a1b2c3d4",
"timestamp": "2026-05-18T10:30:00Z"
}
}Códigos de Error Estándar
| Código | HTTP Status | Descripción |
|---|---|---|
VALIDATION_ERROR | 400 | Datos de entrada inválidos |
UNAUTHORIZED | 401 | No autenticado |
FORBIDDEN | 403 | Sin permisos |
NOT_FOUND | 404 | Recurso no existe |
CONFLICT | 409 | Conflicto de estado |
RATE_LIMITED | 429 | Rate limit excedido |
INTERNAL_ERROR | 500 | Error interno |
SERVICE_UNAVAILABLE | 503 | Servicio no disponible |