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, basic_req_resp_info.
  • Permite controlar la impresión de request desde configuración.
  • 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,
  ecs_elixir_enable_http_req_error: true,
  ecs_elixir_enable_http_req_success: false,
  ecs_elixir_enable_http_resp: true,
  ecs_elixir_http_resp_length: 200

2. Habilitar Plug en el microservicio

En tu endpoint/router Plug-Phoenix agrega el plug para realizar la impresión de errores y éxitos de forma automática:

plug EcsElixirCore.Infra.EntryPoints.PlugHandler.Application.HandlerEcsResponse

Y en application.ex agrega estos children al supervisor:

children = [
  EcsElixirCore.Supervisor,
  # ...otros children
]

3. Registrar errores y éxitos desde el controlador

Realiza el registro de forma manual de error o éxitos desde código.

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)

4. 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,
  ecs_elixir_enable_http_req_error: true,
  ecs_elixir_enable_http_req_success: false,
  ecs_elixir_enable_http_resp: true,
  ecs_elixir_http_resp_length: 200,
  ecs_elixir_enable_basic_req_resp_info: false

Caracteristicas habilitables (flags reales)

ClaveDefaultAplica enEfecto
ecs_elixir_enable_samplingfalseREST + PlugActiva evaluación de sampling antes de escribir el log
ecs_elixir_enable_maskingfalseREST + PlugActiva el enmascaramiento de campos sensibles en la información adicional del log
ecs_elixir_enable_http_req_errorfalseRESTCuando false, elimina campos de request en logs de error
ecs_elixir_enable_http_req_successfalsePlugCuando false, elimina campos de request en logs de exito
ecs_elixir_enable_http_respfalsePlugCuando false, elimina responseBody del log
ecs_elixir_http_resp_length200PlugLimite maximo (1..200) para truncar responseBody serializado
ecs_elixir_enable_basic_req_resp_infofalsePlugControla si se emite el log cuando el evento no contiene requestBody ni responseBody. Ver sección detallada

Claves de sampling avanzado

config :ecs_elixir_core,
  sampling_source_app: :mi_app,
  sampling_source_key: :ecs_sampling

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_http_req_error: true,
  ecs_elixir_enable_http_req_success: false,
  ecs_elixir_enable_http_resp: true,
  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, se debe de garantizar que en árbol de application.ex este configurado el supervisor de la librería:

def start(_type, _args) do
  children = [
    EcsElixirCore.Supervisor,
    # ...
  ]
  Supervisor.start_link(children, strategy: :one_for_one, name: MiApp.Supervisor)
end

Con basic info habilitado

El flag ecs_elixir_enable_basic_req_resp_info controla si los eventos de Plug sin cuerpo (requestBody y responseBody vacíos o ausentes) deben emitirse o descartarse silenciosamente.

Comportamiento del pipeline:

ecs_elixir_enable_basic_req_resp_inforequestBody / responseBody¿Se emite el log?
false (default)ambos ausentes/vacíos❌ descartado (skip)
falseal menos uno presente✅ emitido
truecualquier combinación✅ siempre emitido

Cuándo usarlo:

  • Actívalo (true) si necesitas auditar cada petición, incluso llamadas a endpoints como /health o /ping que no tienen cuerpos relevantes.
  • Déjalo en false (default) para reducir el volumen de logs cuando el evento no aporta información de negocio en el cuerpo.

Ejemplo de configuración:

config :ecs_elixir_core,
  service_name: "mi-microservicio",
  ecs_elixir_enable_basic_req_resp_info: true

Nota: Este flag opera en el pipeline Plug (EcsAppPlugUseCase). Si el log es descartado por este feature, devuelve :ok sin emitir nada — el comportamiento es idéntico al de sampling en modo skip.


El enmascaramiento permite ocular o eliminar el contenido sensible dentro de la información adicional que se incluye en los logs generados:

config :ecs_elixir_core,
  service_name: "mi-microservicio",
  ecs_elixir_enable_masking: true

config :mi_app, :ecs_masking,
  masking_rules_json: ~s([
    {
      "masking_uri_patterns": "/tickets/sell",
      "masking_fields": ["requestBody.*", "password", "requestBody.users.name"],
      "masking_char": "*",
      "masking_type": "custom",
      "masking_percentage": 0.5,
      "masking_custom_placeholder": "[MASKED]",
      "masking_length: 10
    }
    ])

El contenido de la propiedad masking_rules_json debe de ser un Json con la parametrización requerida, por lo que un formato valido es un Json string con las reglas a definir por ejemplo:

"[{\"masking_uri_patterns\":\"/tickets/sell\",\"masking_fields\":[\"requestBody.*\",\"password\",\"requestBody.users.name\"],\"masking_char\":\"*\",\"masking_type\":\"custom\",\"masking_percentage\":0.5,\"masking_custom_placeholder\":\"[MASKED]\",\"masking_length:10}]"

Configuración

PropiedadRequeridoTipoDefaultDescripción
masking_uri_patternsString--Path al cual se le aplicará la regla de enmascaramiento, se admite el uso del comodín *, ej: /tickets/*
masking_fieldsList--Lista de campos a los cuales se les realizara el enmascaramiento, se admite el uso del comodín *
masking_charNoChar*Caracter con el cual sera reemplazado el texto sensible
masking_typeNoStringfullTipo de enmascaramiento a aplicar, valores admitidos: full, partial, custom, remove
masking_percentageNoFloat0.7Porcentaje de enmascaramiento sobre el texto sensible a aplicar cuando el tipo de enmascarado es partial
masking_custom_placeholderNoString[MASKED]Texto personalizada por el cual es reemplazado el texto sensible cuando el tipo de enmascarado es custom
masking_lengthNoInteger8Cantidad de caracteres con los que aparecerán los campos sensibles que han sido enmascarados cuando el tipo de enmascaramiento es full

Tipo enmascaramiento

  • full: Reeamplaza el contenido del texto sensible en su totalidad por el caracter definido en masking_char y su tamaño corresponderá siempre a la longitud definida en masking_length.
  • partical: Reemplaza parcialmente el centido del texto sensible por el caracter definido en masking_char y la porción reemplazada corresponde al porcentaje definido en masking_percentage
  • custom: Reeamplaza el texto sensible por la palabra definida en masking_custom_placeholder.
  • remove: Elimina el campo con el texto sensible del log

Ejemplos de patrones validos

Enmascarar todos los campos del additionalInfo que sean del tipo password

"masking_fields": ["password"]
{
    ...
    "additionalInfo": {
        "uri": "/tickets/sell",
        "requestBody": {
            "password": "***",
            "quantity": "1"
        },
        "responseCode": "200",
        "responseBody": {
            "user": "Doe",
            "password": "***",
            "admin": {
                "user": "Doe",
                "password": "***"
            }
        },
        "responseResult": "OK",
        "method": "POST"
    }
}

Enmascarar el campo password asociado al admin en la respuesta del servicio


"masking_fields": ["responseBody.admin.password"]
{
    ...
    "additionalInfo": {
        "uri": "/tickets/sell",
        "requestBody": {
            "password": "123",
            "quantity": "1"
        },
        "responseCode": "200",
        "responseBody": {
            "user": "Doe",
            "password": "123",
            "admin": {
                "user": "Doe",
                "password": "***"
            }
        },
        "responseResult": "OK",
        "method": "POST"
    }
}

Enmascarar todos los campo asociados al admin

"masking_fields": ["responseBody.admin.*"]
{
    ...
    "additionalInfo": {
        "uri": "/tickets/sell",
        "requestBody": {
            "password": "123",
            "quantity": "1"
        },
        "responseCode": "200",
        "responseBody": {
            "user": "Doe",
            "password": "123",
            "admin": {
                "user": "***",
                "password": "***"
            }
        },
        "responseResult": "OK",
        "method": "POST"
    }
}

Enmascarar el campo password de la lista de usuarios


"masking_fields": ["responseBody.users.password"]
{
    ...
    "additionalInfo": {
        "uri": "/tickets/sell",
        "requestBody": {
            "password": "123",
            "quantity": "1"
        },
        "responseCode": "200",
        "responseBody": {
            "users": [
                {
                    "user": "Doe",
                    "password": "***"
                },
                {
                    "user": "Doe2",
                    "password": "***"
                }
            ]
        },
        "responseResult": "OK",
        "method": "POST"
    }
}

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                 Config REST/base
       plug_logger_config.ex                    Config + async logger para Plug
        features/
            masking/masking_config.ex                Configuracion de masking
            basic_info/basic_info_config.ex          Flag de visibilidad basic info
            request/request_config.ex                Flags de request (error/success)
            response/response_config.ex              Flag de response body
            sampling/sampling_config.ex              Cachea reglas de sampling

 domain/
    model/
       ecs_middleware/
          model/
             core_exception.ex
             ecs_payload.ex
             log_record.ex                    JSON con "message-id" canónico
          value/
              core_exception_input_validator.ex
              core_exception_level_validator.ex
              ecs_build_log_error_constant.ex
              ecs_default_error_constant.ex
              ecs_log_level.ex
              ecs_log_level_constant.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/
       common/
          model/message_id.ex                  Normaliza variantes de message-id
          value/
              command.ex                       Comando entre entry point y use case
              context_data.ex                  Contexto tecnico de ejecucion
       logging/internal_logging.ex              Log interno de la libreria
    use_case/
        ecs_middleware/
           ecs_core_usecase.ex
           ecs_app_rest_usecase.ex
           ecs_app_plug_usecase.ex
        features/
            request/
               request_error_usecase.ex
               request_success_usecase.ex
            basic_info/basic_info_usecase.ex
            response/response_usecase.ex
            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/
         plug_handler/
            application/handler_ecs_response.ex   Plug para logging de responses
            domain/success_response.ex            Response Plug -> EcsPayload
         rest_api/
             application/handler_ecs_rest.ex       API publica: log/3 y log_success/3
             domain/
                 ecs_response.ex                   Error -> EcsPayload
                 success_log.ex                    Exito -> 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
plug~> 1.15Habilitar propiedades sobre Plug
jason~> 1.4Serialización JSON
timex~> 3.7Zona horaria Bogotá
uuid~> 1.1Generación de message-id

Licencia

Apache 2.0 — ver LICENSE.