Build Status Coverage Status Module Version Hex Docs Hex Download Total License
A collection of formatters and utilities for JSON-based logging for various cloud tools and platforms.
-
LoggerJSON.Formatters.Basic
- a basic JSON formatter that logs messages in a structured, but generic format, can be used with any JSON-based logging system. -
LoggerJSON.Formatters.GoogleCloud
- a formatter that logs messages in a structured format that can be consumed by Google Cloud Logger and Google Cloud Error Reporter. -
LoggerJSON.Formatters.Datadog
- a formatter that logs messages in a structured format that can be consumed by Datadog. -
LoggerJSON.Formatters.Elastic
- a formatter that logs messages in a structured format that conforms to the Elastic Common Schema (ECS), so it can be consumed by ElasticSearch, LogStash, FileBeat and Kibana.
Add logger_json
to your list of dependencies in mix.exs
:
def deps do [ # ... {:logger_json, "~> 7.0"} # ... ] end
and install it running mix deps.get
.
Then, enable the formatter in your runtime.exs
:
config :logger, :default_handler, formatter: LoggerJSON.Formatters.Basic.new(metadata: [:request_id])
or inside your application code (eg. in your application.ex
):
formatter = LoggerJSON.Formatters.Basic.new(metadata: :all) :logger.update_handler_config(:default, :formatter, formatter)
or inside your config.exs
(notice that new/1
is not available here
and tuple format must be used):
config :logger, :default_handler, formatter: {LoggerJSON.Formatters.Basic, metadata: [:request_id]}
You might also want to format the log messages when migrations are running:
config :domain, MyApp.Repo, # ... start_apps_before_migration: [:logger_json]
And you might want to make logging level configurable using an LOG_LEVEL
environment variable (in application.ex
):
LoggerJSON.configure_log_level_from_env!()
Additionally, you may also be try redirecting otp reports to Logger (see "Configuration" section).
Configuration can be set using 2nd element of the tuple of the :formatter
option in Logger
configuration.
For example in config.exs
:
config :logger, :default_handler, formatter: LoggerJSON.Formatters.GoogleCloud.new(metadata: :all, project_id: "logger-101")
or during runtime:
formatter = LoggerJSON.Formatters.Basic.new(%{metadata: {:all_except, [:conn]}}) :logger.update_handler_config(:default, :formatter, formatter)
By default, LoggerJSON
is using Jason
as the JSON encoder. If you use Elixir 1.18 or later, you can
use the built-in JSON
module as the encoder. To do this, you need to set the :encoder
option in your
config.exs
file. This setting is only available at compile-time:
config :logger_json, encoder: JSON
The docs can be found at https://hexdocs.pm/logger_json.
{ "message": "Hello", "metadata": { "domain": ["elixir"] }, "severity": "notice", "time": "2024年04月11日T21:31:01.403Z" }
Follows the Google Cloud Logger LogEntry format, for more details see special fields in structured payloads.
{ "logging.googleapis.com/trace": "projects/my-projectid/traces/0679686673a", "logging.googleapis.com/spanId": "000000000000004a", "logging.googleapis.com/operation": { "pid": "#PID<0.29081.0>" }, "logging.googleapis.com/sourceLocation": { "file": "/Users/andrew/Projects/os/logger_json/test/formatters/google_cloud_test.exs", "function": "Elixir.LoggerJSON.Formatters.GoogleCloudTest.test logs an LogEntry of a given level/1", "line": 44 }, "message": { "domain": ["elixir"], "message": "Hello" }, "severity": "NOTICE", "time": "2024年04月12日T15:07:55.020Z" }
and this is how it looks in Google Cloud Logger:
{ "insertId": "1d4hmnafsj7vy1", "jsonPayload": { "message": "Hello", "logging.googleapis.com/spanId": "000000000000004a", "domain": ["elixir"], "time": "2024年04月12日T15:07:55.020Z" }, "resource": { "type": "gce_instance", "labels": { "zone": "us-east1-d", "project_id": "firezone-staging", "instance_id": "3168853301020468373" } }, "timestamp": "2024年04月12日T15:07:55.023307594Z", "severity": "NOTICE", "logName": "projects/firezone-staging/logs/cos_containers", "operation": { "id": "F8WQ1FsdFAm5ZY0AC1PB", "producer": "#PID<0.29081.0>" }, "trace": "projects/firezone-staging/traces/bc007e40a2e9edffa23785d8badc43b8", "sourceLocation": { "file": "lib/phoenix/logger.ex", "line": "231", "function": "Elixir.Phoenix.Logger.phoenix_endpoint_stop/4" }, "receiveTimestamp": "2024年04月12日T15:07:55.678986520Z" }
Exception that can be sent to Google Cloud Error Reporter:
{ "httpRequest": { "protocol": "HTTP/1.1", "referer": "http://www.example.com/", "remoteIp": "", "requestMethod": "PATCH", "requestUrl": "http://www.example.com/", "status": 503, "userAgent": "Mozilla/5.0" }, "logging.googleapis.com/operation": { "pid": "#PID<0.250.0>" }, "logging.googleapis.com/sourceLocation": { "file": "/Users/andrew/Projects/os/logger_json/test/formatters/google_cloud_test.exs", "function": "Elixir.LoggerJSON.Formatters.GoogleCloudTest.test logs exception http context/1", "line": 301 }, "@type": "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent", "context": { "httpRequest": { "protocol": "HTTP/1.1", "referer": "http://www.example.com/", "remoteIp": "", "requestMethod": "PATCH", "requestUrl": "http://www.example.com/", "status": 503, "userAgent": "Mozilla/5.0" }, "reportLocation": { "filePath": "/Users/andrew/Projects/os/logger_json/test/formatters/google_cloud_test.exs", "functionName": "Elixir.LoggerJSON.Formatters.GoogleCloudTest.test logs exception http context/1", "lineNumber": 301 } }, "domain": ["elixir"], "message": "Hello", "serviceContext": { "service": "nonode@nohost" }, "stack_trace": "** (EXIT from #PID<0.250.0>) :foo", "severity": "DEBUG", "time": "2024年04月11日T21:34:53.503Z" }
Adheres to the default standard attribute list as much as possible.
{ "domain": ["elixir"], "http": { "method": "GET", "referer": "http://www.example2.com/", "request_id": null, "status_code": 200, "url": "http://www.example.com/", "url_details": { "host": "www.example.com", "path": "/", "port": 80, "queryString": "", "scheme": "http" }, "useragent": "Mozilla/5.0" }, "logger": { "file_name": "/Users/andrew/Projects/os/logger_json/test/formatters/datadog_test.exs", "line": 239, "method_name": "Elixir.LoggerJSON.Formatters.DatadogTest.test logs http context/1", "thread_name": "#PID<0.225.0>" }, "message": "Hello", "network": { "client": { "ip": "127.0.0.1" } }, "syslog": { "hostname": "MacBook-Pro", "severity": "debug", "timestamp": "2024年04月11日T23:10:47.967Z" } }
Follows the Elastic Common Schema (ECS) format.
{ "@timestamp": "2024年05月21日T15:17:35.374Z", "ecs.version": "8.11.0", "log.level": "info", "log.logger": "Elixir.LoggerJSON.Formatters.ElasticTest", "log.origin": { "file.line": 18, "file.name": "/app/logger_json/test/logger_json/formatters/elastic_test.exs", "function": "test logs message of every level/1" }, "message": "Hello" }
When an error is thrown, the message field is populated with the error message and the error.
fields will be set:
Note: when throwing a custom exception type that defines the fields
id
and/orcode
, then theerror.id
and/orerror.code
fields will be set respectively.
{ "@timestamp": "2024年05月21日T15:20:11.623Z", "ecs.version": "8.11.0", "error.message": "runtime error", "error.stack_trace": "** (RuntimeError) runtime error\n test/logger_json/formatters/elastic_test.exs:191: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\"test logs exceptions\"/1\n", "error.type": "Elixir.RuntimeError", "log.level": "error", "message": "runtime error" }
Any custom metadata fields will be added to the root of the message, so that your application can fill any other ECS fields that you require:
Note that this also allows you to produce messages that do not strictly adhere to the ECS specification.
// Logger.info("Hello") with Logger.metadata(:"device.model.name": "My Awesome Device") // or Logger.info("Hello", "device.model.name": "My Awesome Device") { "@timestamp": "2024年05月21日T15:17:35.374Z", "ecs.version": "8.11.0", "log.level": "info", "log.logger": "Elixir.LoggerJSON.Formatters.ElasticTest", "log.origin": { "file.line": 18, "file.name": "/app/logger_json/test/logger_json/formatters/elastic_test.exs", "function": "test logs message of every level/1" }, "message": "Hello", "device.model.name": "My Awesome Device" }
Copyright (c) 2016 Andrew Dryga
Released under the MIT License, which can be found in LICENSE.md.