ECS Elixir Core
Copy MarkdownLibrería Elixir para generación de logs estructurados bajo el estándar Elastic Common Schema (ECS). Diseñada como middleware de logging para microservicios Elixir/Plug/Phoenix.
Instalación
Agrega ecs_elixir_core a las dependencias en mix.exs:
def deps do
[
{:ecs_elixir_core, "~> 1.0"}
]
endLuego ejecuta:
mix deps.get
Descripción
ecs_elixir_core estandariza cómo los microservicios Elixir registran errores y eventos. Cada log se serializa como JSON y se emite a través del sistema nativo de Elixir (Logger), listo para ser indexado por Elasticsearch/Kibana con el esquema ECS canónico de Bancolombia..
Qué hace la librería:
- Registra errores de negocio con contexto HTTP completo (método, URI, headers, body, código de respuesta).
- Registra respuestas exitosas (2xx) con cuerpo de respuesta.
- Valida y normaliza los datos de entrada antes de construir el log.
- Extrae automáticamente
consumer,message-idy contexto HTTP delconn— el microservicio no los pasa manualmente. - Normaliza el campo
message-iddesde cualquier variante del nombre (messageId,messageid,message_id, etc.). - Aplica features configurables por variable de entorno: sampling, masking.
- Desacopla la tecnología de escritura del dominio mediante un puerto (
LogWriterPort). - Produce JSON idéntico al schema canónico Java (
LogRecord) de la libreríaecs-logs.
Flujo de uso
[Microservicio]
│
├── error → HandlerEcsRest.log(error_map, conn, message_id)
└── éxito → HandlerEcsRest.log_success(result, conn, message_id)
│
Normaliza message_id (cualquier variante → "message-id")
Extrae consumer de conn.req_headers["consumer"]
│
EcsResponse.build / SuccessLog.build_structure
(construye EcsPayload con headers/body como maps)
│
EcsCommand{ payload: EcsPayload, context: Context }
│
EcsAppRestUseCase → EcsCoreUseCase
├── CoreException.new(payload) ← valida campos
├── LogRecord.build_log_record ← arma struct ECS
├── run_features([:sampling, ...])
└── tech_to_print.write(log_record)
│
CmdLineLoggerEcs → Logger.error/info/...(JSON)Inicio rápido
1. Configurar la aplicación
En config/config.exs:
config :ecs_elixir_core,
service_name: "mi-microservicio",
ecs_elixir_enable_sampling: false,
ecs_elixir_enable_masking: false2. Registrar errores y éxitos desde el controlador
alias EcsElixirCore.Infra.EntryPoints.RestApi.Application.HandlerEcsRest
# Log de error — solo campos de negocio; la librería extrae el resto del conn
HandlerEcsRest.log(
%{
code: "ER404-00",
detail: "Recurso no encontrado.",
category: "BEX_ECS_BUG",
log_code: "ER404-00-01",
log_message: "No se encontró el recurso solicitado.",
status: 404,
error: nil
},
conn,
message_id
)
# Log de éxito
HandlerEcsRest.log_success(result, conn, message_id)3. JSON emitido
Error:
{
"message-id": "a1b2c3d4-...",
"date": "03/06/2026 20:19:36:0675",
"service": "mi-microservicio",
"consumer": "APP-MOBILE",
"level": "ERROR",
"additionalInfo": {
"method": "POST",
"uri": "/tickets/sell",
"headers": {
"content-type": "application/json",
"consumer": "APP-MOBILE",
"message-id": "a1b2c3d4-..."
},
"requestBody": { "ticket_id": "T999", "quantity": 1 },
"responseBody": null,
"responseResult": "Not Found",
"responseCode": "404"
},
"error": {
"type": "ER404-00-01",
"message": "Recurso no encontrado.",
"description": "No se encontró el recurso solicitado.",
"optionalInfo": null
}
}Éxito:
{
"message-id": "a1b2c3d4-...",
"date": "03/06/2026 20:19:36:0450",
"service": "mi-microservicio",
"consumer": "APP-MOBILE",
"level": "INFO",
"additionalInfo": {
"method": "POST",
"uri": "/tickets/sell",
"headers": { "consumer": "APP-MOBILE", "content-type": "application/json" },
"requestBody": { "ticket_id": "T001", "quantity": 2 },
"responseBody": { "sale_id": "uuid-...", "status": "CONFIRMED" },
"responseResult": "OK",
"responseCode": "200"
}
}Configuración
Básica
config :ecs_elixir_core,
service_name: "mi-microservicio",
ecs_elixir_enable_sampling: false,
ecs_elixir_enable_masking: falseCon sampling habilitado
El sampling permite reducir el volumen de logs en endpoints de alta frecuencia. Se configuran reglas por URI y código de respuesta:
config :ecs_elixir_core,
service_name: "mi-microservicio",
ecs_elixir_enable_sampling: true,
ecs_elixir_enable_masking: false,
sampling_source_app: :mi_app,
sampling_source_key: :ecs_sampling
config :mi_app, :ecs_sampling,
rules20XJson: ~s([
{"uri": "/health", "responseCode": "200", "showCount": 1, "skipCount": 9},
{"uri": "/tickets/sell", "responseCode": "200", "showCount": 1, "skipCount": 4}
]),
rules40XJson: ~s([
{"uri": "/tickets/sell", "responseCode": "404",
"errorCodes": "ER404-00|ER404-01", "showCount": 1, "skipCount": 4}
])Cuando el sampling está habilitado, agregar el GenServer al árbol de supervisión en application.ex:
alias EcsElixirCore.Infra.DrivenAdapters.EtsGenServer.Features.Sampling.Infra.EtsSamplingGenServer
def start(_type, _args) do
children = [
EtsSamplingGenServer,
# ...
]
Supervisor.start_link(children, strategy: :one_for_one, name: MiApp.Supervisor)
endUso
Log de error — HandlerEcsRest.log/3
HandlerEcsRest.log(error_map, conn, message_id)| Campo | Tipo | Descripción |
|---|---|---|
code | String | Código de error de negocio |
detail | String | Mensaje para el usuario |
category | String | Categoría del error |
log_code | String | Código de trazabilidad del log |
log_message | String | Mensaje interno del log |
status | integer | Código de estado HTTP |
error | any | Excepción original (se captura en optionalInfo) |
Log de éxito — HandlerEcsRest.log_success/3
HandlerEcsRest.log_success(result, conn, message_id)result puede ser cualquier término — se serializa e incluye en responseBody.
Normalización de message-id
La librería acepta cualquiera de estas variantes y siempre emite la clave canónica "message-id":
HandlerEcsRest.log(error, conn, "abc-123") # string explícito
HandlerEcsRest.log(error, conn, nil) # extraído automáticamente del conn
# Nombres de header aceptados: message-id, messageId, messageid, message_idNiveles de log
| Nivel | Función Logger | Cuándo usarlo |
|---|---|---|
DEBUG | Logger.debug | Trazabilidad en desarrollo |
INFO | Logger.info | Respuestas exitosas, eventos normales |
WARNING | Logger.warning | Errores recuperables |
ERROR | Logger.error | Errores de negocio controlados |
CRITICAL | Logger.critical | Fallas graves |
Ejemplo completo en un controlador
defmodule MiApp.TicketController do
use MiApp, :controller
alias EcsElixirCore.Infra.EntryPoints.RestApi.Application.HandlerEcsRest
def sell(conn, params) do
message_id = conn |> get_req_header("message-id") |> List.first()
case MiApp.TicketService.sell(params) do
{:ok, result} ->
HandlerEcsRest.log_success(result, conn, message_id)
conn |> put_status(:ok) |> json(result)
{:error, :not_found} ->
error = %{
code: "ER404-00",
detail: "Recurso no encontrado.",
category: "ERROR",
log_code: "ER404-00-01",
log_message: "No se encontró el ticket solicitado.",
status: 404,
error: nil
}
HandlerEcsRest.log(error, conn, message_id)
conn |> put_status(:not_found) |> json(%{error: error.detail})
{:error, exception} ->
error = %{
code: "SAER500-29",
detail: "Ha ocurrido un error inesperado.",
category: "ERROR",
log_code: "SAER500-29-01",
log_message: "Error inesperado al procesar la venta.",
status: 500,
error: exception
}
HandlerEcsRest.log(error, conn, message_id)
conn |> put_status(:internal_server_error) |> json(%{error: error.detail})
end
end
endArquitectura
lib/
├── application/
│ ├── ecs_middleware/middleware_ecs_config.ex Lee service_name y features
│ └── features/
│ ├── sampling/sampling_config.ex Cachea reglas de sampling
│ └── masking/masking_config.ex Configuración de masking
│
├── domain/
│ ├── model/
│ │ ├── ecs_middleware/
│ │ │ ├── model/
│ │ │ │ ├── core_exception.ex
│ │ │ │ ├── ecs_constant.ex
│ │ │ │ ├── ecs_payload.ex
│ │ │ │ └── log_record.ex JSON con "message-id" canónico
│ │ │ └── value/
│ │ │ ├── core_exception_field_validator.ex
│ │ │ ├── core_exception_level_validator.ex
│ │ │ └── log_record_error.ex
│ │ └── features/sampling/
│ │ ├── model/sampling_rule_set.ex
│ │ └── value/
│ │ ├── sampling_error_code.ex
│ │ ├── sampling_rule.ex
│ │ └── sampling_rule_validator.ex
│ ├── shared/
│ │ ├── logging/internal_logging.ex Log interno de la librería
│ │ └── model/common/
│ │ ├── ecs_command.ex Comando entre entry point y use case
│ │ └── message_id.ex Normaliza variantes de message-id
│ └── use_case/
│ ├── ecs_middleware/
│ │ ├── ecs_core_usecase.ex
│ │ ├── ecs_app_rest_usecase.ex
│ │ └── ports/log_writer_port.ex Behaviour para adaptadores de escritura
│ └── features/sampling/sampling_usecase.ex
│
└── infra/
├── driven_adapters/
│ ├── cmd_line/middleware_ecs/application/
│ │ └── cmd_line_logger_ecs.ex Implementa LogWriterPort
│ └── ets_gen_server/features/sampling/infra/
│ └── ets_sampling_gen_server.ex Contador ETS para sampling
└── entry_points/rest_api/
├── application/handler_ecs_rest.ex API pública: log/3 y log_success/3
└── domain/
├── ecs_response.ex Error → EcsPayload
└── success_log.ex Éxito → EcsPayloadDesarrollo local
git clone https://github.com/bancolombia/ecs-elixir-core.git
cd ecs-elixir-core
mix deps.get
mix test
mix coveralls.html # reporte de cobertura
mix credo --strict # análisis estático
mix dialyzer # verificación de tipos
Dependencias
| Librería | Versión | Uso |
|---|---|---|
jason | ~> 1.4 | Serialización JSON |
timex | ~> 3.7 | Zona horaria Bogotá |
uuid | ~> 1.1 | Generación de message-id |
Licencia
Apache 2.0 — ver LICENSE.