Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow setting Datadog syslog.hostname attribute #87

Merged
merged 4 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions lib/logger_json.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ defmodule LoggerJSON do

## Log Format

LoggerJSON provides two JSON formatters out of the box.
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