Skip to content

Datadog formatter crashes with BadMapError when metadata contains non-map error: key #168

@gorkunov

Description

@gorkunov

Bug Description

LoggerJSON.Formatters.Datadog crashes with a BadMapError when a Logger.error call includes error: as a metadata key with a non-map value (e.g., a string or tuple).

Affected Version

v7.0.3 (and likely all 7.x)

Root Cause

In lib/logger_json/formatters/datadog.ex lines 266-270, format_error/4 does:

defp format_error(%{message: message}, metadata, level, reported_levels) when is_binary(message) do
  if level in reported_levels do
    error =
      metadata[:error]
      |> Kernel.||(%{})
      |> Map.put(:kind, get_error_kind(metadata))
      |> Map.put(:message, message)
      |> maybe_put(:stack, get_error_stack(metadata))

    %{error: error}
  end
end

The Kernel.||(%{}) fallback only triggers when metadata[:error] is nil or false. If a user passes any truthy non-map value (string, tuple, atom, etc.), the || does not fall through, and Map.put is called on a non-map — crashing with:

** (BadMapError) expected a map, got: "some error message"

This produces FORMATTER CRASH entries in production logs, silently swallowing the actual error log output.

How to Reproduce

Logger.error("Something failed", error: "connection refused")
Logger.error("Something failed", error: {:timeout, "request timed out"})

Any Logger.error (or :critical/:alert/:emergency) call with a non-map error: metadata key triggers the crash.

Note: error: is a very natural metadata key name for logging, so users are likely to hit this without realizing the Datadog formatter reserves it for structured error tracking.

Suggested Fix

Add a guard or pattern match to ensure metadata[:error] is a map before merging into it:

defp format_error(%{message: message}, metadata, level, reported_levels) when is_binary(message) do
  if level in reported_levels do
    base =
      case metadata[:error] do
        %{} = map -> map
        _other -> %{}
      end

    error =
      base
      |> Map.put(:kind, get_error_kind(metadata))
      |> Map.put(:message, message)
      |> maybe_put(:stack, get_error_stack(metadata))

    %{error: error}
  end
end

The same defensive check should be applied to get_error_kind/1 and get_error_stack/1 — though those already pattern-match on maps so they are safe today.

Workaround

Avoid using error: as a top-level Logger metadata key, or ensure it is always a map conforming to Datadog's error tracking format (%{kind: ..., message: ..., stack: ...}). We renamed all our error: metadata keys to error_details: as a workaround.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions