Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a34385d
Support `:stacktrace` metadata introduced in Ecto 3.8.0
fuelen Apr 27, 2022
7d846c4
Handle `nil` as `NULL`
fuelen May 1, 2022
a8fe155
Don't return to original color when log stacktrace
fuelen May 31, 2022
f7629f3
Ignore content of the previously inlined parameters
fuelen May 31, 2022
3924d55
Make inline_params private
fuelen May 31, 2022
78bf247
Support `Ecto.Adapters.MyXQL` adapter
fuelen Jun 23, 2022
57b4e98
Bump version to 0.3.0
fuelen Jun 24, 2022
68284fe
Multiple Repo support (#11)
sh41 Jun 26, 2022
4ed6e6f
Bump version to 0.4.0
fuelen Jun 26, 2022
56f3114
Don't log migration checks
fuelen Jul 14, 2022
c984bc1
Bump version to 0.4.1
fuelen Jul 14, 2022
317a470
Use `:ansi_enabled` configuration value
fuelen Aug 1, 2022
cca2b36
Bump version to 0.4.2
fuelen Aug 1, 2022
2c8dd17
Fix support of child list as inline params serialize (#13)
simonprev Aug 30, 2022
fc27ad7
Bump version to 0.4.3
fuelen Aug 30, 2022
c15488d
Support Time struct (#15)
haste Oct 13, 2022
c34372d
Bump version to 0.5.0
fuelen Oct 13, 2022
bef1be8
Support Postgrex network types
fuelen Oct 28, 2022
6c0d306
Bump version to 0.6.0
fuelen Nov 2, 2022
543b6c5
Update deps
fuelen Nov 3, 2022
549dedc
Add Ecto.DevLogger.PrintableParameter protocol, closes #16
fuelen Nov 4, 2022
9cc8579
Add hex badge to README
fuelen Nov 6, 2022
815c7b0
Bump version to 0.7.0
fuelen Nov 6, 2022
d6baa6c
Fix typo
fuelen Nov 6, 2022
1c7f3d0
Convert rest of the atoms to strings
fuelen Nov 28, 2022
486c2bb
Wrap JSON using string quotes
fuelen Dec 9, 2022
f27e57a
Add option to specify callback to silence logs, closes #18
fuelen Jan 6, 2023
8f30a6d
Support tsvector
fuelen Feb 11, 2023
2f11ca8
Bump version to 0.9.0
fuelen Feb 12, 2023
53b42ba
Support Postgrex.Interval (#27)
vanderhoop Dec 12, 2023
d5b4884
Don't fail if there is no protocol implementation
fuelen Dec 12, 2023
0c3a017
Bump version to `0.10.0`
fuelen Dec 12, 2023
da2131a
Restricted Installation Instructions (#29)
halostatue Dec 15, 2023
d8bbda8
Update deps
fuelen Jun 21, 2024
61a688b
Add test for list of numeric enums
fuelen Jun 21, 2024
40c5d16
Add `:before_inline_callback` option for queries formatting
fuelen Jun 21, 2024
f73ea06
Bump version to `0.11.0`
fuelen Jun 21, 2024
9e38519
Fix ex_doc deprecation warning
fuelen Jun 21, 2024
096905e
Fix broken tests (#31)
jechol Jul 26, 2024
ef2ff81
Add geo point and geo polygon (#32)
javiercr Jul 30, 2024
c64d9a4
Format files
fuelen Jul 30, 2024
c806827
Update deps
fuelen Jul 30, 2024
afa6a96
Bump version to `0.12.0`
fuelen Jul 30, 2024
98144e8
Add comments after numeric enums (#33)
dkuku Aug 2, 2024
463418e
Use uuids from cast params, closes #9 (#34)
dkuku Aug 2, 2024
42a4b29
Bump version to `0.13.0`
fuelen Aug 2, 2024
c9a5195
SQLite3 Support (#36)
sh41 Oct 22, 2024
4491c27
Update deps
fuelen Oct 22, 2024
f72083d
Bump version to `0.14.0`
fuelen Oct 22, 2024
14c02d1
Dependecy Update (#37)
alimakki Nov 13, 2024
e1f4bec
Bump version to `0.14.1`
fuelen Nov 13, 2024
2e7347c
Add PostgreSQL range type support
fuelen Sep 13, 2025
f6e8daf
Add PostgreSQL multirange support and tests
fuelen Sep 13, 2025
0868e03
Add proper support for geo (postgis), add option to ignore a single log
fuelen Sep 13, 2025
d3e1ad5
Improve grammar
fuelen Sep 13, 2025
534ebfb
Add downloads badge to README
fuelen Sep 13, 2025
e5b0d78
Make NumericEnum module private
fuelen Sep 13, 2025
f7854aa
Update deps
fuelen Sep 13, 2025
a78791b
Update docs for Ecto.DevLogger.PrintableParameter protocol
fuelen Sep 13, 2025
93f6df3
Update docs
fuelen Sep 14, 2025
8314016
Bump version to `0.15.0`
fuelen Sep 14, 2025
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
167 changes: 161 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# Ecto.DevLogger

[![Hex.pm](https://img.shields.io/hexpm/v/ecto_dev_logger.svg)](https://hex.pm/packages/ecto_dev_logger)
[![Hex.pm Downloads](https://img.shields.io/hexpm/dt/ecto_dev_logger)](https://hex.pm/packages/ecto_dev_logger)

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 representation (binary UUID, DateTime, Decimal, json, etc).
Also, it highlights db time to make slow queries noticeable. Source table and inlined bindings are highlighted as well.
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](./assets/screenshot.png)

Expand All @@ -16,24 +19,176 @@ The package can be installed by adding `ecto_dev_logger` to your list of depende
```elixir
def deps do
[
{:ecto_dev_logger, "~> 0.1"}
{:ecto_dev_logger, "~> 0.15"}
]
end
```

Then disable default logger for your repo in config file for dev mode:
Then disable the default logger for your repo in the config file for development:
```elixir
if config_env() == :dev do
config :my_app, MyApp.Repo, log: false
end
```
And install telemetry handler in `MyApp.Application`:
Then install the telemetry handler in `MyApp.Application`:
```elixir
Ecto.DevLogger.install(MyApp.Repo)
```
Telemetry handler will be installed only if `log` configuration value is set to `false`.
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](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`:

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

In `MyApp.Application`, an additional function is required:

```elixir
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`:

```elixir
# 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.Type`s 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](https://github.com/darold/pgFormatter) as an external utility:
```elixir
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:

```console
$ 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:

```console
$ docker run -p5432:5432 --rm --name ecto_dev_logger_postgis -e POSTGRES_PASSWORD=postgres -d postgis/postgis
```
Binary file modified assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading