Packages

Generate structured logs from telemetry events with built-in support for popular libraries

Current section

Files

Jump to
telemetry_logger lib telemetry_logger.ex
Raw

lib/telemetry_logger.ex

defmodule TelemetryLogger do
@moduledoc """
Defines a behaviour for loggers based on telemetry events.
It also contains a helper function `attach_loggers/1` to attach to the
`:telemetry` runtime and start logging results.
"""
@callback init(keyword()) ::
{:ok, [:telemetry.event_name()], :telemetry.handler_config()}
| {:error, term()}
@callback handle_event(
:telemetry.event_name(),
:telemetry.event_measurements(),
:telemetry.event_metadata(),
:telemetry.handler_config()
) ::
{:log, Logger.level(), Logger.message(), Logger.metadata() | %{atom() => term()}}
| :skip
@doc """
Attaches to `:telemetry` and forward events to telemetry loggers and then
calls Logger properly.
The values on the logger list are passed to `init/1` callback.
Example:
TelemetryLogger.attach_loggers(
MyLogger,
{AnotherLogger, something: 1234}
)
Will result in calls `MyLogger.init([])` and `AnotherLogger.init(something:
1234)` which will then be passed as the last argument to
`MyLogger.handle_event/4` and `AnotherLogger.handle_event/4` respectively.
"""
@spec attach_loggers([module() | {module(), keyword()}]) ::
:ok | {:error, {:already_exists, [{atom(), keyword()}]}}
def attach_loggers(loggers) do
results =
for {logger, opts} <- normalize_loggers(loggers) do
{:ok, events, config} = logger.init(opts)
result =
:telemetry.attach_many(
{__MODULE__, logger, opts},
events,
&__MODULE__.handle_event/4,
%{logger: logger, logger_config: config}
)
{result, logger, opts}
end
existing = for {{:error, :already_exists}, logger, opts} <- results, do: {logger, opts}
case existing do
[] -> :ok
_ -> {:error, {:already_exists, existing}}
end
end
defp normalize_loggers(loggers) do
Enum.map(loggers, fn
{logger, opts} when is_atom(logger) ->
{logger, opts}
logger when is_atom(logger) ->
{logger, []}
config ->
raise "Invalid Logger config #{inspect(config)}.\nExpecting either a module or a `{mod, opts}` tuple."
end)
end
@doc false
def handle_event(
event_name,
measurements,
metadata,
%{
logger: logger,
logger_config: logger_config
},
log_fn \\ &Logger.bare_log/3
) do
:ok
case logger.handle_event(event_name, measurements, metadata, logger_config) do
{:log, level, message, metadata} ->
log_fn.(level, message, metadata)
:ok
:skip ->
:ok
end
end
end