ECS Elixir Core

Hex.pmDocsLicenciaCI

Librerí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"}
]
end

Luego 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:


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: false

2. 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: false

Con 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)
end

Uso

Log de error — HandlerEcsRest.log/3

HandlerEcsRest.log(error_map, conn, message_id)
CampoTipoDescripción
codeStringCódigo de error de negocio
detailStringMensaje para el usuario
categoryStringCategoría del error
log_codeStringCódigo de trazabilidad del log
log_messageStringMensaje interno del log
statusintegerCódigo de estado HTTP
erroranyExcepció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_id

Niveles de log

NivelFunción LoggerCuándo usarlo
DEBUGLogger.debugTrazabilidad en desarrollo
INFOLogger.infoRespuestas exitosas, eventos normales
WARNINGLogger.warningErrores recuperables
ERRORLogger.errorErrores de negocio controlados
CRITICALLogger.criticalFallas 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
end

Arquitectura

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 → EcsPayload

Desarrollo 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íaVersiónUso
jason~> 1.4Serialización JSON
timex~> 3.7Zona horaria Bogotá
uuid~> 1.1Generación de message-id

Licencia

Apache 2.0 — ver LICENSE.