Skip to content

fuelen/ecto_dev_logger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

70 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ecto.DevLogger

Hex.pm Hex.pm Downloads

An alternative logger for Ecto queries.

It inlines bindings into the query, so it is easy to copy-paste logged SQL and run it in any IDE for debugging without manual transformation of common Elixir terms to string representations (binary UUID, DateTime, Decimal, JSON, etc.). It also highlights DB time to make slow queries noticeable. The source table and inlined bindings are highlighted as well.

before and after

Installation

The package can be installed by adding ecto_dev_logger to your list of dependencies in mix.exs:

def deps do
  [
    {:ecto_dev_logger, "~> 0.15"}
  ]
end

Then disable the default logger for your repo in the config file for development:

if config_env() == :dev do
  config :my_app, MyApp.Repo, log: false
end

Then install the telemetry handler in MyApp.Application:

Ecto.DevLogger.install(MyApp.Repo)

The telemetry handler will be installed only if the repo :log configuration is set to false.

That's it.

The docs can be found at https://hexdocs.pm/ecto_dev_logger.

Development Only Installation

If you turn off repo logging for any reason in production, you can configure ecto_dev_logger to only be available in development. In your mix.exs, restrict the installation to :dev:

def deps do
  [
    {:ecto_dev_logger, "~> 0.10", only: :dev}
  ]
end

In MyApp.Application, an additional function is required:

defmodule MyApp.Application do
  @moduledoc "..."

  def start(_type, _args) do
    maybe_install_ecto_dev_logger()

    # ...
  end

  if Code.ensure_loaded?(Ecto.DevLogger) do
    defp maybe_install_ecto_dev_logger, do: Ecto.DevLogger.install(MyApp.Repo)
  else
    defp maybe_install_ecto_dev_logger, do: :ok
  end

  # ...
end

Ignore logging for a single Repo call

If you want to suppress logging for a specific query or Repo operation, pass log: false via telemetry_options:

# Examples
Repo.query!("CREATE EXTENSION IF NOT EXISTS postgis", [], telemetry_options: [log: false])
Repo.insert!(changeset, telemetry_options: [log: false])
Repo.get!(User, user_id, telemetry_options: [log: false])

This prevents Ecto.DevLogger from emitting a log for that telemetry event while still executing the operation normally.

How it works and limitations

Ecto.DevLogger inlines query parameters by converting Elixir values into SQL expressions. It does this by calling the Ecto.DevLogger.PrintableParameter protocol for each bound value, producing a copy‑pastable literal or expression.

Because it only sees Elixir values (not the database column types), it must guess the target database type. The mapping from Elixir types to database types is not one‑to‑one, so the output may not always match your schema exactly:

  • Maps: assumed to be JSON. If you store maps in other column types (for example, hstore when using postgrex), the rendered SQL will still be JSON.
  • Lists: assumed to be array‑like columns; you might instead be storing lists as JSON.
  • Scalars: integers, floats, booleans, and strings are logged as plain values.

If you use custom database or driver‑level types, implement Ecto.DevLogger.PrintableParameter for the structs that appear in parameters to control how values are rendered and keep the logged SQL runnable. Note that Ecto.DevLogger operates below Ecto.Type casting; multiple different Ecto.Types can map to the same driver type. The logger sees the post‑cast value (for example, a Postgrex.* struct), not your Ecto.Type.

Keep in mind that the logged SQL is meant for debugging; it aims to be helpful, but you may still need to add manual casts to match your schema precisely.

Rendering examples

Below are examples of how common Elixir values are rendered in logged SQL:

Elixir value Rendered SQL Notes
nil NULL
true / false true / false
"hello" 'hello' Strings are single-quoted
<<1, 2, 3>> DECODE('AQID','BASE64') Non‑UTF‑8 binaries use a base64 decode function
123 123 Integers are unquoted
12.34 12.34 Floats are unquoted
Decimal.new("12.34") 12.34 Decimals are unquoted
~D[2023-01-02] '2023-01-02' Dates are quoted strings
~U[2023-01-02 03:04:05Z] '2023-01-02 03:04:05Z' DateTimes are quoted strings
~N[2023-01-02 03:04:05] '2023-01-02 03:04:05' NaiveDateTimes are quoted strings
~T[03:04:05] '03:04:05' Times are quoted strings
%{"a" => 1} '{"a":1}' Maps are rendered as JSON strings
["Elixir", "Ecto"] '{Elixir,Ecto}' Array string literal when all elements are string‑renderable
["Elixir", <<153>>] ARRAY['Elixir', DECODE('mQ==','BASE64')] Falls back to ARRAY[...] if mixed
{"Elixir", "Ecto"} '(Elixir,Ecto)' Composite string literal when all elements are string‑renderable
{"Elixir", <<153>>} ROW('Elixir', DECODE('mQ==','BASE64')) Falls back to ROW(...) if mixed
%Postgrex.INET{address: {127,0,0,1}, netmask: 24} '127.0.0.1/24' IP/netmask rendered as text
%Postgrex.MACADDR{address: {8,1,43,5,7,9}} '08:01:2B:05:07:09' MAC address rendered as text
%Postgrex.Interval{months: 1, days: 2, secs: 34} '1 mon 2 days 34:00:00' Interval rendered via Postgrex.Interval.to_string/1
%Postgrex.Range{lower: 1, upper: 10, lower_inclusive: true, upper_inclusive: false} '[1,10)' Range bounds and brackets
%Postgrex.Range{lower: :empty} 'empty' Empty range
%Postgrex.Multirange{ranges: [...]} '{[1,3),(10,15]}' Multirange of ranges
[%Postgrex.Lexeme{}, ...] 'word1:pos weight ...' Lists of lexemes are rendered as tsvector strings

Notes:

  • “String‑renderable” means PrintableParameter.to_string_literal/1 returns a string for the element. Otherwise, to_expression/1 is used.
  • Unknown structs (without a PrintableParameter implementation) fall back to inspect/1 and may not form valid SQL.

Geo rendering examples (optional)

Below are examples when the geo library is available:

Geo value Rendered SQL
%Geo.Point{coordinates: {1.0, 2.0}, srid: 4326} 'SRID=4326;POINT(1.0 2.0)'
%Geo.PointZ{coordinates: {1.0, 2.0, 3.0}} 'POINT Z(1.0 2.0 3.0)'
%Geo.PointM{coordinates: {1.0, 2.0, 4.0}} 'POINT M(1.0 2.0 4.0)'
%Geo.PointZM{coordinates: {1.0, 2.0, 3.0, 4.0}} 'POINT ZM(1.0 2.0 3.0 4.0)'
%Geo.LineString{coordinates: [{0.0, 0.0}, {1.0, 1.0}]} 'LINESTRING(0.0 0.0,1.0 1.0)'
%Geo.LineStringZ{coordinates: [{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}]} 'LINESTRINGZ(0.0 0.0 0.0,1.0 1.0 1.0)'
%Geo.LineStringZM{coordinates: [{0.0, 0.0, 0.0, 5.0}, {1.0, 1.0, 1.0, 6.0}]} 'LINESTRINGZM(0.0 0.0 0.0 5.0,1.0 1.0 1.0 6.0)'
%Geo.Polygon{coordinates: [[{0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {0.0, 0.0}]]} 'POLYGON((0.0 0.0,0.0 1.0,1.0 1.0,0.0 0.0))'
%Geo.PolygonZ{coordinates: [[{0.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {1.0, 1.0, 0.0}, {0.0, 0.0, 0.0}]]} 'POLYGON((0.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0,0.0 0.0 0.0))'
%Geo.MultiPoint{coordinates: [{0.0, 0.0}, {1.0, 1.0}]} 'MULTIPOINT(0.0 0.0,1.0 1.0)'
%Geo.MultiPointZ{coordinates: [{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}]} 'MULTIPOINTZ(0.0 0.0 0.0,1.0 1.0 1.0)'
%Geo.MultiLineString{coordinates: [[{0.0, 0.0}, {1.0, 1.0}]]} 'MULTILINESTRING((0.0 0.0,1.0 1.0))'
%Geo.MultiLineStringZ{coordinates: [[{0.0, 0.0, 0.0}, {1.0, 1.0, 1.0}]]} 'MULTILINESTRINGZ((0.0 0.0 0.0,1.0 1.0 1.0))'
%Geo.MultiPolygon{coordinates: [[[{0.0, 0.0}, {0.0, 1.0}, {1.0, 1.0}, {0.0, 0.0}]]}] 'MULTIPOLYGON(((0.0 0.0,0.0 1.0,1.0 1.0,0.0 0.0)))'
%Geo.MultiPolygonZ{coordinates: [[[{0.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {1.0, 1.0, 0.0}, {0.0, 0.0, 0.0}]]}] 'MULTIPOLYGONZ(((0.0 0.0 0.0,0.0 1.0 0.0,1.0 1.0 0.0,0.0 0.0 0.0)))'
%Geo.GeometryCollection{geometries: [%Geo.Point{coordinates: {1.0, 2.0}}, %Geo.LineString{coordinates: [{0.0, 0.0}, {1.0, 1.0}]}]} 'GEOMETRYCOLLECTION(POINT(1.0 2.0),LINESTRING(0.0 0.0,1.0 1.0))'

Format queries

It is possible to format queries using the :before_inline_callback option. Here is an example setup using pgFormatter as an external utility:

defmodule MyApp.Application do
  def start(_type, _args) do
    Ecto.DevLogger.install(MyApp.Repo, before_inline_callback: &__MODULE__.format_sql_query/1)
  end

  def format_sql_query(query) do
    case System.shell("echo $SQL_QUERY | pg_format -", env: [{"SQL_QUERY", query}], stderr_to_stdout: true) do
      {formatted_query, 0} -> String.trim_trailing(formatted_query)
      _ -> query
    end
  end
end

Running tests

You need to run a local PostgreSQL server for the tests to interact with. This is one way to do it:

$ docker run -p5432:5432 --rm --name ecto_dev_logger_postgres -e POSTGRES_PASSWORD=postgres -d postgres

If you want PostGIS enabled (for geometry types and extensions), run a PostGIS image instead:

$ docker run -p5432:5432 --rm --name ecto_dev_logger_postgis -e POSTGRES_PASSWORD=postgres -d postgis/postgis

About

An alternative logger for Ecto queries

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 14

Languages