Skip to content

Commit

Permalink
Do not write warnings when telemetry, Ecto or Plug are not available
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewDryga committed May 21, 2024
1 parent 09f8164 commit 1a72c79
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 82 deletions.
158 changes: 80 additions & 78 deletions lib/logger_json/ecto.ex
Original file line number Diff line number Diff line change
@@ -1,94 +1,96 @@
defmodule LoggerJSON.Ecto do
@moduledoc """
A telemetry handler that logs Ecto query metrics in JSON format.
if Code.ensure_loaded?(Ecto) and Code.ensure_loaded?(:telemetry) do
defmodule LoggerJSON.Ecto do
@moduledoc """
A telemetry handler that logs Ecto query metrics in JSON format.
This module is not recommended to be used in production, as it can be
costly to log every single database query.
"""
require Logger
This module is not recommended to be used in production, as it can be
costly to log every single database query.
"""
require Logger

@doc """
Attaches the telemetry handler to the given event.
@doc """
Attaches the telemetry handler to the given event.
### Available options
### Available options
* `:level` - log level which is used to log requests. Defaults to `:info`.
* `:level` - log level which is used to log requests. Defaults to `:info`.
### Dynamic log level
### Dynamic log level
In some cases you may wish to set the log level dynamically
on a per-query basis. To do so, set the `:level` option to
a tuple, `{Mod, Fun, Args}`. The query and map of time measures
will be prepended to the provided list of arguments.
In some cases you may wish to set the log level dynamically
on a per-query basis. To do so, set the `:level` option to
a tuple, `{Mod, Fun, Args}`. The query and map of time measures
will be prepended to the provided list of arguments.
When invoked, your function must return a
[`Logger.level()`](`t:Logger.level()/0`) or `false` to
disable logging for the request.
When invoked, your function must return a
[`Logger.level()`](`t:Logger.level()/0`) or `false` to
disable logging for the request.
### Examples
### Examples
Attaching the telemetry handler to the `MyApp.Repo` events with the `:info` log level:
Attaching the telemetry handler to the `MyApp.Repo` events with the `:info` log level:
LoggerJSON.Ecto.attach("logger-json-queries", [:my_app, :repo, :query], :info)
LoggerJSON.Ecto.attach("logger-json-queries", [:my_app, :repo, :query], :info)
For more details on event and handler naming see
(`Ecto.Repo` documentation)[https://hexdocs.pm/ecto/Ecto.Repo.html#module-telemetry-events].
"""
@spec attach(
name :: String.t(),
event :: [atom()],
level ::
Logger.level()
| {module :: module(), function :: atom(), arguments :: [term()]}
| false
) :: :ok | {:error, :already_exists}
def attach(name, event, level) do
:telemetry.attach(name, event, &__MODULE__.telemetry_logging_handler/4, level)
end
For more details on event and handler naming see
(`Ecto.Repo` documentation)[https://hexdocs.pm/ecto/Ecto.Repo.html#module-telemetry-events].
"""
@spec attach(
name :: String.t(),
event :: [atom()],
level ::
Logger.level()
| {module :: module(), function :: atom(), arguments :: [term()]}
| false
) :: :ok | {:error, :already_exists}
def attach(name, event, level) do
:telemetry.attach(name, event, &__MODULE__.telemetry_logging_handler/4, level)
end

@doc """
A telemetry handler that logs Ecto query along with it's metrics in a structured format.
"""
@spec telemetry_logging_handler(
event_name :: [atom()],
measurements :: %{
query_time: non_neg_integer(),
queue_time: non_neg_integer(),
decode_time: non_neg_integer(),
total_time: non_neg_integer()
},
metadata :: %{required(:query) => String.t(), required(:repo) => module()},
level ::
Logger.level()
| {module :: module(), function :: atom(), arguments :: [term()]}
| false
) :: :ok
def telemetry_logging_handler(_event_name, measurements, %{query: query, repo: repo}, level) do
query_time = Map.get(measurements, :query_time) |> format_time(:nanosecond)
queue_time = Map.get(measurements, :queue_time) |> format_time(:nanosecond)
decode_time = Map.get(measurements, :decode_time) |> format_time(:nanosecond)
latency = Map.get(measurements, :total_time) |> format_time(:nanosecond)

metadata = [
query: %{
repo: inspect(repo),
execution_time_μs: query_time,
decode_time_μs: decode_time,
queue_time_μs: queue_time,
latency_μs: latency
}
]

if level = level(level, query, measurements) do
Logger.log(level, query, metadata)
else
:ok
@doc """
A telemetry handler that logs Ecto query along with it's metrics in a structured format.
"""
@spec telemetry_logging_handler(
event_name :: [atom()],
measurements :: %{
query_time: non_neg_integer(),
queue_time: non_neg_integer(),
decode_time: non_neg_integer(),
total_time: non_neg_integer()
},
metadata :: %{required(:query) => String.t(), required(:repo) => module()},
level ::
Logger.level()
| {module :: module(), function :: atom(), arguments :: [term()]}
| false
) :: :ok
def telemetry_logging_handler(_event_name, measurements, %{query: query, repo: repo}, level) do
query_time = Map.get(measurements, :query_time) |> format_time(:nanosecond)
queue_time = Map.get(measurements, :queue_time) |> format_time(:nanosecond)
decode_time = Map.get(measurements, :decode_time) |> format_time(:nanosecond)
latency = Map.get(measurements, :total_time) |> format_time(:nanosecond)

metadata = [
query: %{
repo: inspect(repo),
execution_time_μs: query_time,
decode_time_μs: decode_time,
queue_time_μs: queue_time,
latency_μs: latency
}
]

if level = level(level, query, measurements) do
Logger.log(level, query, metadata)
else
:ok
end
end
end

defp level({m, f, a}, query, measurements), do: apply(m, f, [query, measurements | a])
defp level(level, _query, _measurements) when is_atom(level), do: level
defp level({m, f, a}, query, measurements), do: apply(m, f, [query, measurements | a])
defp level(level, _query, _measurements) when is_atom(level), do: level

defp format_time(nil, _unit), do: 0
defp format_time(time, unit), do: System.convert_time_unit(time, unit, :microsecond)
defp format_time(nil, _unit), do: 0
defp format_time(time, unit), do: System.convert_time_unit(time, unit, :microsecond)
end
end
3 changes: 2 additions & 1 deletion lib/logger_json/formatters/basic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ defmodule LoggerJSON.Formatters.Basic do
"time" => "2024-04-11T21:31:01.403Z"
}
"""
import Jason.Helpers, only: [json_map: 1]
import LoggerJSON.Formatter.{MapBuilder, DateTime, Message, Metadata, RedactorEncoder}

@behaviour LoggerJSON.Formatter
Expand Down Expand Up @@ -72,6 +71,8 @@ defmodule LoggerJSON.Formatters.Basic do
end

if Code.ensure_loaded?(Plug.Conn) do
import Jason.Helpers, only: [json_map: 1]

defp format_http_request(%{conn: %Plug.Conn{} = conn}) do
json_map(
connection:
Expand Down
3 changes: 2 additions & 1 deletion lib/logger_json/formatters/datadog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ defmodule LoggerJSON.Formatters.Datadog do
}
}
"""
import Jason.Helpers, only: [json_map: 1]
import LoggerJSON.Formatter.{MapBuilder, DateTime, Message, Metadata, Code, RedactorEncoder}

@behaviour LoggerJSON.Formatter
Expand Down Expand Up @@ -195,6 +194,8 @@ defmodule LoggerJSON.Formatters.Datadog do
defp safe_chardata_to_string(other), do: other

if Code.ensure_loaded?(Plug.Conn) do
import Jason.Helpers, only: [json_map: 1]

defp format_http_request(%{conn: %Plug.Conn{} = conn} = meta) do
request_url = Plug.Conn.request_url(conn)
user_agent = LoggerJSON.Formatter.Plug.get_header(conn, "user-agent")
Expand Down
3 changes: 2 additions & 1 deletion lib/logger_json/formatters/elastic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ defmodule LoggerJSON.Formatters.Elastic do
}
```
"""
import Jason.Helpers, only: [json_map: 1]
import LoggerJSON.Formatter.{MapBuilder, DateTime, Message, Metadata, RedactorEncoder}

@behaviour LoggerJSON.Formatter
Expand Down Expand Up @@ -223,6 +222,8 @@ defmodule LoggerJSON.Formatters.Elastic do
defp format_logger_fields(_meta), do: nil

if Code.ensure_loaded?(Plug.Conn) do
import Jason.Helpers, only: [json_map: 1]

# See the formats for the following fields in ECS:
# - client.ip: https://www.elastic.co/guide/en/ecs/8.11/ecs-client.html
# - http.*: https://www.elastic.co/guide/en/ecs/8.11/ecs-http.html
Expand Down
2 changes: 1 addition & 1 deletion lib/logger_json/plug.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
if Code.ensure_loaded?(Plug) do
if Code.ensure_loaded?(Plug) and Code.ensure_loaded?(:telemetry) do
defmodule LoggerJSON.Plug do
@moduledoc """
A telemetry handler that logs request information in JSON format.
Expand Down

0 comments on commit 1a72c79

Please sign in to comment.