Elixir ECS Library
A comprehensive Elixir library that enables structured logging in ECS (Elastic Common Schema) format, providing standardized log output for better observability and monitoring in Elixir applications.
Installation
Add ecs_logs_elixir to your list of dependencies in mix.exs:
def deps do
[
{:ecs_logs_elixir, , "~> 0.1.0"}
]
endThen run:
mix deps.getConfiguration
Service Configuration
Configure your service name, sampling_source_app, and sampling_source_key in config/config.exs:
config :ecs_logs_elixir,
service_name: "my_application",
sampling_source_app: :my_application,
sampling_source_key: :ecs_samplingservice_name: name of the service reported in the ECS payload. If not defined, the default value is"INDEFINIDO".sampling_source_app: name of the application where the sampling configuration is stored. Default::ecs_logs_elixir.sampling_source_key: configuration key used to read sampling rules. Default::ecs_sampling.
Sampling
It allows you to reduce the number of ECS logs written for selected requests instead of logging every matching event. Sampling rules are loaded from the application defined in sampling_source_app and the key defined in sampling_source_key.
Each sampling rule contains:
uri: endpoint path to match.responseCode: HTTP response code used to classify the rule.showCount: number of matching logs to print.skipCount: number of matching logs to skip.errorCodes: pipe-separated error codes used only for40Xrules.
Sampling Configuration
In environment-specific files such as config/dev.exs, config/test.exs, and config/pdn.exs, define the sampling rules under the application and key configured by sampling_source_app and sampling_source_key.
Minimal structure:
# sampling
config :my_application, :ecs_sampling,
rules20XJson: "[{\"uri\":\"/signin\",\"responseCode\":\"200\",\"showCount\":1,\"skipCount\":1}]",
rules40XJson: "[{\"uri\":\"/signin\",\"responseCode\":\"401\",\"showCount\":1,\"skipCount\":1,\"errorCodes\":\"ER-401\"},{\"uri\":\"/signup\",\"responseCode\":\"409\",\"showCount\":1,\"skipCount\":3,\"errorCodes\":\"ER-409\"}]"rules20XJson
Use this variable to define sampling rules for endpoints that return 20X HTTP status codes.
Valid JSON value:
[
{
"uri": "/signin",
"responseCode": "200",
"showCount": 1,
"skipCount": 1
}
]
This rule means that for /signin with HTTP 200, the library prints 1 log and skips 1 log. In practice, matching requests are logged 50% of the time.
How it must look inside Elixir config:
rules20XJson: "[{\"uri\":\"/signin\",\"responseCode\":\"200\",\"showCount\":1,\"skipCount\":1}]"
You can also use an empty string or an empty JSON array if you do not want sampling rules for 20X responses.
rules40XJson
Use this variable to define sampling rules for endpoints that return 40X HTTP status codes.
Valid JSON value:
[
{
"uri": "/signin",
"responseCode": "401",
"showCount": 1,
"skipCount": 1,
"errorCodes": "ER-401"
},
{
"uri": "/signup",
"responseCode": "409",
"showCount": 1,
"skipCount": 3,
"errorCodes": "ER-409"
},
{
"uri": "/signup",
"responseCode": "400",
"showCount": 1,
"skipCount": 1,
"errorCodes": "ER-400"
},
{
"uri": "/signin",
"responseCode": "404",
"showCount": 1,
"skipCount": 1,
"errorCodes": "ER-404"
}
]
These rules define sampling for 40X responses using the derived error code. For example, /signup with derived error code ER-409 prints 1 log and skips 3, so matching requests are logged 25% of the time.
How it must look inside Elixir config:
rules40XJson: "[{\"uri\":\"/signin\",\"responseCode\":\"401\",\"showCount\":1,\"skipCount\":1,\"errorCodes\":\"ER-401\"},{\"uri\":\"/signup\",\"responseCode\":\"409\",\"showCount\":1,\"skipCount\":3,\"errorCodes\":\"ER-409\"},{\"uri\":\"/signup\",\"responseCode\":\"400\",\"showCount\":1,\"skipCount\":1,\"errorCodes\":\"ER-400\"},{\"uri\":\"/signin\",\"responseCode\":\"404\",\"showCount\":1,\"skipCount\":1,\"errorCodes\":\"ER-404\"}]"
You can also use an empty string or an empty JSON array if you do not want sampling rules for 40X responses.
Because both values are JSON arrays stored as strings, double quotes must be escaped inside Elixir config.
Sampling Runtime Behavior
For 20X responses:
-
Rules are matched with the key
"#{uri}|#{responseCode}". errorCodesmust not be configured. If present, it must be empty.
For 40X responses:
-
Rules are matched with the key
"#{uri}|#{derived_error_code}". errorCodesis required and must contain one or more pipe-separated values.-
The derived error code is built from the first two segments of
internal_error_code. -
Example:
"ER-409-01-01"becomes"ER-409".
General behavior:
cycle = showCount + skipCount.-
A log is printed when the current counter position is lower than
showCount. - Counters rotate within the configured cycle for each rule key.
- If no matching rule is found, the log is printed.
- If sampling configuration is missing, invalid, or cannot be parsed, the log is printed.
- Any response not covered by a configured rule is logged normally.
Sampling Validation Notes
rules20XJsononly accepts rules whoseresponseCodestarts with20.rules40XJsononly accepts rules whoseresponseCodestarts with40.showCountandskipCountmust be non-negative integers.showCount + skipCountmust be greater than0.- An empty string or an empty JSON array means no sampling rules are applied for that group.
Usage
Basic Logging
# Simple error logging
ElixirEcsLogger.log_ecs(%{
error_code: "USER_001",
error_message: "User validation failed",
additional_info: %{
uri: "/users",
responseCode: 400
}
})
# Logging with additional details
ElixirEcsLogger.log_ecs(%{
error_code: "DB_001",
error_message: "Database connection timeout",
level: "ERROR",
internal_error_code: "CONN_TIMEOUT",
internal_error_message: "Connection to database timed out after 30 seconds",
additional_details: %{
database: "users_db",
timeout: 30000,
retry_count: 3
},
message_id: "msg_12345",
consumer: "user_service",
additional_info: %{
uri: "/users",
responseCode: 500
}
})Log Levels
The library supports the following log levels:
"DEBUG": detailed information for debugging."INFO": general information messages."WARNING": warning messages for potential issues."ERROR": error messages for handled exceptions."CRITICAL": critical errors that may cause application failure.
# Debug level logging
ElixirEcsLogger.log_ecs(%{
error_code: "DEBUG_001",
error_message: "Processing user request",
level: "DEBUG",
additional_info: %{
uri: "/users",
responseCode: 200
}
})
# Critical level logging
ElixirEcsLogger.log_ecs(%{
error_code: "CRIT_001",
error_message: "Database connection lost",
level: "CRITICAL",
internal_error_code: "DB-500-01",
internal_error_message: "Primary database is unreachable",
additional_info: %{
uri: "/users",
responseCode: 500
}
})Full Example
defmodule MyApp.UserService do
def create_user(params) do
case validate_user(params) do
{:ok, user} ->
ElixirEcsLogger.log_ecs(%{
error_code: "USER_CREATED",
error_message: "User successfully created",
level: "INFO",
message_id: generate_message_id(),
consumer: "user_service",
additional_details: %{user_id: user.id},
additional_info: %{
uri: "/users",
responseCode: 201
}
})
{:ok, user}
{:error, reason} ->
ElixirEcsLogger.log_ecs(%{
error_code: "USER_VALIDATION_FAILED",
error_message: "User validation failed",
level: "ERROR",
internal_error_code: "VALIDATION_ERROR",
internal_error_message: inspect(reason),
additional_details: %{params: params},
message_id: generate_message_id(),
consumer: "user_service",
additional_info: %{
uri: "/users",
responseCode: 400
}
})
{:error, reason}
end
end
endApplication Integration
Add ECS logging in both your global response and error handlers.
Success Handler
require ElixirEcsLogger
log_success(response, conn, headers, request_body)
defp log_success(%{status: status, body: response_body}, conn, headers, request_body) do
payload = build_ecs_payload(conn, status, headers, request_body, response_body)
ElixirEcsLogger.log_ecs(payload)
end
defp build_ecs_payload(conn, status, headers, request_body, response_body) do
%{
error_code: "",
error_message: "",
level: "INFO",
internal_error_code: "",
internal_error_message: "",
additional_details: "",
message_id: Map.get(headers, "message_id", ""),
consumer: nil,
additional_info: build_additional_info(conn, status, headers, request_body, response_body)
}
end
defp build_additional_info(conn, status, headers, body, response_body) do
%{
method: conn.method,
uri: conn.request_path,
headers: headers,
requestBody: body,
responseBody: response_body,
responseResult: "OK",
responseCode: status
}
endError Handler
require ElixirEcsLogger
@status_descriptions %{
400 => "Bad Request",
401 => "Unauthorized",
404 => "Not Found",
409 => "Conflict",
500 => "Internal Server Error"
}
log_error(exception_data, conn, headers, body)
defp log_error(exception_data, conn, headers, body) do
payload = build_ecs_payload(exception_data, conn, headers, body)
ElixirEcsLogger.log_ecs(payload)
end
defp build_ecs_payload(exception_data, conn, headers, body) do
%{
error_code: exception_data.code,
error_message: exception_data.detail,
level: "ERROR",
internal_error_code: exception_data.log_code,
internal_error_message: exception_data.log_message,
additional_details: "",
message_id: Map.get(headers, "message_id", ""),
consumer: nil,
additional_info: build_additional_info(conn, exception_data.status, headers, body)
}
end
defp build_additional_info(conn, status, headers, body) do
%{
method: conn.method,
uri: conn.request_path,
headers: headers,
requestBody: body,
responseResult: status_description(status),
responseCode: status
}
end
defp status_description(status), do: Map.get(@status_descriptions, status, "Unknown Error")API Documentation
ElixirEcsLogger.log_ecs/1
Logs a structured message in ECS format.
Parameters:
attrs(map) - Logging attributes
Required attributes:
error_code(string) - Unique error code identifier. Required forERROR,WARNING, andCRITICALlevels.error_message(string) - Human-readable error message. Required forERROR,WARNING, andCRITICALlevels.additional_info(map) - Contextual data included in the ECS payload. For sampling decisions,additional_info.uriandadditional_info.responseCodeare required.
Optional attributes:
level(string) - Log level (defaults to "ERROR")internal_error_code(string) - Internal system error codeinternal_error_message(string) - Internal system error messageadditional_details(any) - Additional context informationmessage_id(string) - Unique message identifierconsumer(string) - Service or component that generated the log
Returns:
:ok- Successfully logged{:error, reason}- Error occurred during logging
Log Output Format
The library generates JSON logs with the following structure:
{
"messageId": "12345",
"date": "29/10/2025 17:48:55.734000",
"service": "my_application",
"consumer": "user_service",
"additionalInfo": {
"uri": "/users",
"responseCode": 400
},
"level": "ERROR",
"error": {
"type": "VALIDATION_ERROR",
"message": "User validation failed",
"description": "Email format is invalid",
"optionalInfo": {
"field": "email",
"value": "invalid-email"
}
}
}
For non-error levels such as INFO and DEBUG, the error object is omitted from the final JSON payload.
Development
Running Tests
# Run all tests
mix test
# Run with coverage
mix coveralls.htmlCode Quality
# Run code formatter
mix format
# Run static analysis
mix credo
# Run dialyzer
mix dialyzerContributing
Contributions are welcome.
- Fork the repository.
- Create a feature branch.
- Make your changes.
- Add tests for new functionality.
- Ensure tests and checks pass.
- Open a Pull Request.
Development Guidelines
- Follow Elixir naming conventions.
- Write tests for new behavior.
- Update documentation when the public API changes.
- Ensure code passes formatting and static analysis checks.
- Add typespecs for public functions where appropriate.