ECS Elixir Core

Copy Markdown

Hex.pm Docs Licencia CI

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:

  • 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-id y contexto HTTP del conn — el microservicio no los pasa manualmente.
  • Normaliza el campo message-id desde 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ía ecs-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:  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.