Skip to content

Commit

Permalink
feat: allow setting Datadog syslog.hostname attribute (#87)
Browse files Browse the repository at this point in the history
* Remove syslog hostname from datadog formatter

* feat: support init/1 callback and formatter_state

* feat: allow setting datadog syslog.hostname attribute

* convert formatter_opts to a map
  • Loading branch information
btkostner authored Jun 21, 2022
1 parent fcff19e commit cad53fe
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 28 deletions.
23 changes: 17 additions & 6 deletions lib/logger_json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ defmodule LoggerJSON do
LoggerJSON provides three JSON formatters out of the box.
You can change this structure by implementing `LoggerJSON.Formatter` behaviour and passing module
name to `:formatter` config option. Example implementations can be found in `LoggerJSON.Formatters.GoogleCloudLogger`
and `LoggerJSON.Formatters.BasicLogger`.
name to `:formatter` config option. Example implementations can be found in `LoggerJSON.Formatters.GoogleCloudLogger`,
`LoggerJSON.Formatters.DatadogLogger`, and `LoggerJSON.Formatters.BasicLogger`.
config :logger_json, :backend,
formatter: MyFormatterImplementation
If your formatter supports different options, you can specify them with `:formatter_opts`.
config :logger_json, :backend,
formatter: LoggerJSON.Formatters.DatadogLogger,
formatter_opts: [hostname: "example.com"]
## Encoders support
You can replace default Jason encoder with other module that supports `encode!/1` function. This can be even used
Expand Down Expand Up @@ -55,7 +61,8 @@ defmodule LoggerJSON do
output: nil,
json_encoder: nil,
on_init: nil,
formatter: nil
formatter: nil,
formatter_state: %{}

@doc """
Configures Logger log level at runtime by using value from environment variable.
Expand Down Expand Up @@ -197,14 +204,18 @@ defmodule LoggerJSON do
|> Keyword.get(:metadata, [])
|> configure_metadata()

formatter_opts = Keyword.get(config, :formatter_opts, %{})
formatter_state = apply(formatter, :init, [formatter_opts])

%{
state
| metadata: metadata,
level: level,
device: device,
max_buffer: max_buffer,
json_encoder: json_encoder,
formatter: formatter
formatter: formatter,
formatter_state: formatter_state
}
end

Expand Down Expand Up @@ -259,15 +270,15 @@ defmodule LoggerJSON do
end

defp format_event(level, msg, ts, md, state) do
%{json_encoder: json_encoder, formatter: formatter, metadata: md_keys} = state
%{json_encoder: json_encoder, formatter: formatter, formatter_state: formatter_state, metadata: md_keys} = state

unless formatter do
raise ArgumentError,
"invalid :formatter option for :logger_json application. " <>
"Expected module name that implements LoggerJSON.Formatter behaviour, " <> "got: #{inspect(json_encoder)}"
end

event = formatter.format_event(level, msg, ts, md, md_keys)
event = formatter.format_event(level, msg, ts, md, md_keys, formatter_state)

case json_encoder do
nil ->
Expand Down
10 changes: 9 additions & 1 deletion lib/logger_json/formatter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ defmodule LoggerJSON.Formatter do
`LoggerJSON.Formatters.BasicLogger`.
"""

@doc """
Initialization callback. Ran on startup with the given `formatter_opts` list.
Returned list will be used as formatter_state in `format_event/6`.
"""
@callback init(Keyword.t()) :: Keyword.t()

@doc """
Format event callback.
Expand All @@ -16,6 +23,7 @@ defmodule LoggerJSON.Formatter do
msg :: Logger.message(),
ts :: Logger.Formatter.time(),
md :: [atom] | :all,
state :: map
state :: map,
formatter_state :: map
) :: map | iodata() | %Jason.Fragment{}
end
5 changes: 4 additions & 1 deletion lib/logger_json/formatters/basic_logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ defmodule LoggerJSON.Formatters.BasicLogger do
@processed_metadata_keys ~w[pid file line function module application]a

@impl true
def format_event(level, msg, ts, md, md_keys) do
def init(_formatter_opts), do: []

@impl true
def format_event(level, msg, ts, md, md_keys, _formatter_state) do
json_map(
time: FormatterUtils.format_timestamp(ts),
severity: Atom.to_string(level),
Expand Down
71 changes: 59 additions & 12 deletions lib/logger_json/formatters/datadog_logger.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,51 @@
defmodule LoggerJSON.Formatters.DatadogLogger do
@moduledoc """
DataDog formatter.
[DataDog](https://www.datadoghq.com) formatter. This will adhere to the
[default standard attribute list](https://docs.datadoghq.com/logs/processing/attributes_naming_convention/#default-standard-attribute-list)
as much as possible.
## Options
This formatter has a couple of options to fine tune the logging output for
your deployed environment.
### `hostname`
By setting the `hostname` value, you can change how the `syslog.hostname` is
set in logs. In most cases, you can leave this unset and it will use default
to `:system`, which uses `:inet.gethostname/0` to resolve the value.
If you are running in an environment where the hostname is not correct, you
can hard code it by setting `hostname` to a string. In places where the
hostname is inaccurate but also dynamic (like Kubernetes), you can set
`hostname` to `:unset` to exclude it entirely. You'll then be relying on
[`dd-agent`](https://docs.datadoghq.com/agent/) to determine the hostname.
Adhere to the
[default standard attribute list](https://docs.datadoghq.com/logs/processing/attributes_naming_convention/#default-standard-attribute-list).
"""
import Jason.Helpers, only: [json_map: 1]

alias LoggerJSON.{FormatterUtils, JasonSafeFormatter}

@behaviour LoggerJSON.Formatter

@default_opts %{hostname: :system}
@processed_metadata_keys ~w[pid file line function module application span_id trace_id]a

def format_event(level, msg, ts, md, md_keys) do
@impl true
def init(formatter_opts \\ %{}) do
opts = Map.merge(@default_opts, formatter_opts)

unless is_binary(opts.hostname) or opts.hostname in [:system, :unset] do
raise ArgumentError,
"invalid :hostname option for :formatter_opts logger_json backend. " <>
"Expected :system, :unset, or string, " <> "got: #{inspect(opts.hostname)}"
end

opts
end

@impl true
def format_event(level, msg, ts, md, md_keys, formatter_state) do
Map.merge(
%{
logger:
Expand All @@ -24,12 +56,7 @@ defmodule LoggerJSON.Formatters.DatadogLogger do
line: Keyword.get(md, :line)
),
message: IO.chardata_to_string(msg),
syslog:
json_map(
hostname: node_hostname(),
severity: Atom.to_string(level),
timestamp: FormatterUtils.format_timestamp(ts)
)
syslog: syslog(level, ts, formatter_state.hostname)
},
format_metadata(md, md_keys)
)
Expand Down Expand Up @@ -62,8 +89,28 @@ defmodule LoggerJSON.Formatters.DatadogLogger do
FormatterUtils.format_function(module, function)
end

defp node_hostname do
defp syslog(level, ts, :system) do
{:ok, hostname} = :inet.gethostname()
to_string(hostname)

json_map(
hostname: to_string(hostname),
severity: Atom.to_string(level),
timestamp: FormatterUtils.format_timestamp(ts)
)
end

defp syslog(level, ts, :unset) do
json_map(
severity: Atom.to_string(level),
timestamp: FormatterUtils.format_timestamp(ts)
)
end

defp syslog(level, ts, hostname) do
json_map(
hostname: hostname,
severity: Atom.to_string(level),
timestamp: FormatterUtils.format_timestamp(ts)
)
end
end
9 changes: 7 additions & 2 deletions lib/logger_json/formatters/google_cloud_logger.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
{:error, "ERROR"}
]

@impl true
def init(_formatter_opts), do: []

@doc """
Builds structured payload which is mapped to Google Cloud Logger
[`LogEntry`](https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry) format.
See: https://cloud.google.com/logging/docs/agent/configuration#special_fields_in_structured_payloads
"""
for {level, gcp_level} <- @severity_levels do
def format_event(unquote(level), msg, ts, md, md_keys) do
@impl true
def format_event(unquote(level), msg, ts, md, md_keys, _formatter_state) do
Map.merge(
%{
time: FormatterUtils.format_timestamp(ts),
Expand All @@ -38,7 +42,8 @@ defmodule LoggerJSON.Formatters.GoogleCloudLogger do
end
end

def format_event(_level, msg, ts, md, md_keys) do
@impl true
def format_event(_level, msg, ts, md, md_keys, _formatter_state) do
Map.merge(
%{
time: FormatterUtils.format_timestamp(ts),
Expand Down
3 changes: 2 additions & 1 deletion test/unit/logger_json/ecto_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ defmodule LoggerJSON.EctoTest do
metadata: [],
json_encoder: Jason,
on_init: :disabled,
formatter: LoggerJSON.Formatters.GoogleCloudLogger
formatter: LoggerJSON.Formatters.GoogleCloudLogger,
formatter_state: %{}
)

diff = :erlang.convert_time_unit(1, :microsecond, :native)
Expand Down
3 changes: 2 additions & 1 deletion test/unit/logger_json/plug_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ defmodule LoggerJSON.PlugTest do
metadata: :all,
json_encoder: Jason,
on_init: :disabled,
formatter: LoggerJSON.Formatters.GoogleCloudLogger
formatter: LoggerJSON.Formatters.GoogleCloudLogger,
formatter_state: %{}
)
end

Expand Down
3 changes: 2 additions & 1 deletion test/unit/logger_json_basic_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ defmodule LoggerJSONBasicTest do
metadata: [],
json_encoder: Jason,
on_init: :disabled,
formatter: BasicLogger
formatter: BasicLogger,
formatter_state: %{}
)
end

Expand Down
37 changes: 36 additions & 1 deletion test/unit/logger_json_datadog_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ defmodule LoggerJSONDatadogTest do
metadata: [],
json_encoder: Jason,
on_init: :disabled,
formatter: DatadogLogger
formatter: DatadogLogger,
formatter_state: %{}
)

:ok = Logger.reset_metadata([])
Expand Down Expand Up @@ -280,6 +281,40 @@ defmodule LoggerJSONDatadogTest do
end) == ""
end

test "logs hostname when set to :system" do
Logger.configure_backend(LoggerJSON, formatter_opts: %{hostname: :system})
{:ok, hostname} = :inet.gethostname()

log =
fn -> Logger.debug("hello") end
|> capture_log()
|> Jason.decode!()

assert log["syslog"]["hostname"] == to_string(hostname)
end

test "does not log hostname when set to :unset" do
Logger.configure_backend(LoggerJSON, formatter_opts: %{hostname: :unset})

log =
fn -> Logger.debug("hello") end
|> capture_log()
|> Jason.decode!()

assert log["syslog"]["hostname"] == nil
end

test "logs hostname when set to string" do
Logger.configure_backend(LoggerJSON, formatter_opts: %{hostname: "testing"})

log =
fn -> Logger.debug("hello") end
|> capture_log()
|> Jason.decode!()

assert log["syslog"]["hostname"] == "testing"
end

test "logs severity" do
log =
fn -> Logger.debug("hello") end
Expand Down
3 changes: 2 additions & 1 deletion test/unit/logger_json_google_error_reporter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ defmodule LoggerJSONGoogleErrorReporterTest do
metadata: :all,
json_encoder: Jason,
on_init: :disabled,
formatter: GoogleCloudLogger
formatter: GoogleCloudLogger,
formatter_state: %{}
)

:ok = Logger.reset_metadata([])
Expand Down
3 changes: 2 additions & 1 deletion test/unit/logger_json_google_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ defmodule LoggerJSONGoogleTest do
metadata: [],
json_encoder: Jason,
on_init: :disabled,
formatter: GoogleCloudLogger
formatter: GoogleCloudLogger,
formatter_state: %{}
)

:ok = Logger.reset_metadata([])
Expand Down

0 comments on commit cad53fe

Please sign in to comment.