Skip to content

Commit 88f27a8

Browse files
authored
Add SQLite3 support ans meta table (#31)
This change adds support for SQLite3 storage of errors and includes a new `error_tracker_meta` table to store metadata needed for ErrorTracker to function properly - like managing migration versions. Aside from adding support to SQLite3, which was as easy as expected, we have also added a way to track migration versions that should work in any SQL database (although only PostgreSQL and SQLite3 are currently supported). That KV store table will also work in the future to store runtime configuration, for example. In this change we are also migrating PostgreSQL systems to use this new table, which means migrating from using table comments to store migration version to use this new table. As for SQLite3 migrations, migrations start at `V02` to maintain the same version IDs on all platforms.
1 parent c8c73c3 commit 88f27a8

File tree

15 files changed

+324
-96
lines changed

15 files changed

+324
-96
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ error_tracker-*.tar
2929

3030

3131
dev.local.exs
32+
dev.db*

dev.exs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ Logger.configure(level: :debug)
1313
Code.require_file("dev.local.exs")
1414

1515
# Prepare the repo
16+
17+
adapter =
18+
case Application.get_env(:error_tracker, :ecto_adapter) do
19+
:postgres -> Ecto.Adapters.Postgres
20+
:sqlite3 -> Ecto.Adapters.SQLite3
21+
end
22+
1623
defmodule ErrorTrackerDev.Repo do
17-
use Ecto.Repo, otp_app: :error_tracker, adapter: Ecto.Adapters.Postgres
24+
use Ecto.Repo, otp_app: :error_tracker, adapter: adapter
1825
end
1926

20-
_ = Ecto.Adapters.Postgres.storage_up(ErrorTrackerDev.Repo.config())
27+
_ = adapter.storage_up(ErrorTrackerDev.Repo.config())
2128

2229
# Configures the endpoint
2330
Application.put_env(:error_tracker, ErrorTrackerDevWeb.Endpoint,

dev.local.example.exs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1-
# Prepare the Repo URL
1+
# PostgreSQL adapter
2+
#
3+
# To use SQLite3 on your local development machine uncomment these lines and
4+
# comment the lines of other adapters.
5+
6+
Application.put_env(:error_tracker, :ecto_adapter, :postgres)
7+
28
Application.put_env(
39
:error_tracker,
410
ErrorTrackerDev.Repo,
511
url: "ecto://postgres:[email protected]/error_tracker_dev"
612
)
13+
14+
# SQlite3 adapter
15+
#
16+
# To use SQLite3 on your local development machine uncomment these lines and
17+
# comment the lines of other adapters.
18+
19+
# Application.put_env(:error_tracker, :ecto_adapter, :sqlite3)
20+
21+
# sqlite_db = System.get_env("SQLITE_DB") || "dev.db"
22+
# Application.put_env(:error_tracker, ErrorTrackerDev.Repo, database: sqlite_db)

guides/Getting Started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This guide is an introduction to ErrorTracker, an Elixir-based built-in error tr
44

55
In this guide we will learn how to install ErrorTracker in an Elixir project so you can start reporting errors as soon as possible. We will also cover more advanced topics such as how to report custom errors and how to add extra context to reported errors.
66

7-
**This guide requires you to have set up Ecto with PostgreSQL beforehand.**
7+
**This guide requires you to have set up Ecto with PostgreSQL or SQLite3 beforehand.**
88

99
## Installing the ErrorTracker as a dependency
1010

lib/error_tracker.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ defmodule ErrorTracker do
1616
1717
## Requirements
1818
19-
ErrorTracker requires Elixir 1.15+, Ecto 3.11+, Phoenix LiveView 0.19+, and PostgreSQL.
19+
ErrorTracker requires Elixir 1.15+, Ecto 3.11+, Phoenix LiveView 0.19+, and
20+
PostgreSQL or SQLite3 as database.
2021
2122
## Integrations
2223

lib/error_tracker/migration.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ defmodule ErrorTracker.Migration do
5555
mix ecto.migrate
5656
```
5757
58-
## Custom prefix
58+
## Custom prefix - PostgreSQL only
5959
6060
ErrorTracker supports namespacing its own tables using PostgreSQL schemas, also known
6161
as "prefixes" in Ecto. With prefixes your error tables can reside outside of your primary
@@ -112,6 +112,7 @@ defmodule ErrorTracker.Migration do
112112
defp migrator do
113113
case ErrorTracker.Repo.__adapter__() do
114114
Ecto.Adapters.Postgres -> ErrorTracker.Migration.Postgres
115+
Ecto.Adapters.SQLite3 -> ErrorTracker.Migration.SQLite
115116
adapter -> raise "ErrorTracker does not support #{adapter}"
116117
end
117118
end

lib/error_tracker/migration/postgres.ex

Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,80 +4,28 @@ defmodule ErrorTracker.Migration.Postgres do
44
@behaviour ErrorTracker.Migration
55

66
use Ecto.Migration
7-
8-
import Ecto.Query
7+
alias ErrorTracker.Migration.SQLMigrator
98

109
@initial_version 1
11-
@current_version 1
10+
@current_version 2
1211
@default_prefix "public"
1312

1413
@impl ErrorTracker.Migration
1514
def up(opts) do
1615
opts = with_defaults(opts, @current_version)
17-
initial = current_version(opts)
18-
19-
cond do
20-
initial == 0 ->
21-
change(@initial_version..opts.version, :up, opts)
22-
23-
initial < opts.version ->
24-
change((initial + 1)..opts.version, :up, opts)
25-
26-
true ->
27-
:ok
28-
end
16+
SQLMigrator.migrate_up(__MODULE__, opts, @initial_version)
2917
end
3018

3119
@impl ErrorTracker.Migration
3220
def down(opts) do
3321
opts = with_defaults(opts, @initial_version)
34-
initial = max(current_version(opts), @initial_version)
35-
36-
if initial >= opts.version do
37-
change(initial..opts.version, :down, opts)
38-
end
22+
SQLMigrator.migrate_down(__MODULE__, opts, @initial_version)
3923
end
4024

4125
@impl ErrorTracker.Migration
4226
def current_version(opts) do
4327
opts = with_defaults(opts, @initial_version)
44-
repo = Map.get_lazy(opts, :repo, fn -> repo() end)
45-
46-
query =
47-
from pg_class in "pg_class",
48-
left_join: pg_description in "pg_description",
49-
on: pg_description.objoid == pg_class.oid,
50-
left_join: pg_namespace in "pg_namespace",
51-
on: pg_namespace.oid == pg_class.relnamespace,
52-
where: pg_class.relname == "error_tracker_errors",
53-
where: pg_namespace.nspname == ^opts.escaped_prefix,
54-
select: pg_description.description
55-
56-
case repo.one(query, log: false) do
57-
version when is_binary(version) -> String.to_integer(version)
58-
_other -> 0
59-
end
60-
end
61-
62-
defp change(versions_range, direction, opts) do
63-
for version <- versions_range do
64-
padded_version = String.pad_leading(to_string(version), 2, "0")
65-
66-
migration_module = Module.concat(__MODULE__, "V#{padded_version}")
67-
apply(migration_module, direction, [opts])
68-
end
69-
70-
case direction do
71-
:up -> record_version(opts, Enum.max(versions_range))
72-
:down -> record_version(opts, Enum.min(versions_range) - 1)
73-
end
74-
end
75-
76-
defp record_version(%{prefix: prefix}, version) do
77-
case version do
78-
0 -> :ok
79-
_other -> execute "COMMENT ON TABLE #{inspect(prefix)}.error_tracker_errors IS '#{version}'"
80-
end
28+
SQLMigrator.current_version(opts)
8129
end
8230

8331
defp with_defaults(opts, version) do

lib/error_tracker/migration/postgres/v01.ex

Lines changed: 68 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,83 @@ defmodule ErrorTracker.Migration.Postgres.V01 do
33

44
use Ecto.Migration
55

6-
def up(%{create_schema: create_schema, prefix: prefix}) do
7-
if create_schema, do: execute("CREATE SCHEMA IF NOT EXISTS #{prefix}")
8-
9-
create table(:error_tracker_errors,
10-
primary_key: [name: :id, type: :bigserial],
11-
prefix: prefix
12-
) do
13-
add :kind, :string, null: false
14-
add :reason, :text, null: false
15-
add :source_line, :text, null: false
16-
add :source_function, :text, null: false
17-
add :status, :string, null: false
18-
add :fingerprint, :string, null: false
19-
add :last_occurrence_at, :utc_datetime_usec, null: false
20-
21-
timestamps(type: :utc_datetime_usec)
22-
end
6+
import Ecto.Query
237

24-
create unique_index(:error_tracker_errors, [:fingerprint], prefix: prefix)
8+
def up(opts = %{create_schema: create_schema, prefix: prefix}) do
9+
# Prior to V02 the migration version was stored in table comments.
10+
# As of now the migration version is stored in a new table (created in V02).
11+
#
12+
# However, systems migrating to V02 may think they need to run V01 too, so
13+
# we need to check for the legacy version storage to avoid running this
14+
# migration twice.
15+
if current_version_legacy(opts) == 0 do
16+
if create_schema, do: execute("CREATE SCHEMA IF NOT EXISTS #{prefix}")
2517

26-
create table(:error_tracker_occurrences,
27-
primary_key: [name: :id, type: :bigserial],
28-
prefix: prefix
29-
) do
30-
add :context, :map, null: false
31-
add :reason, :text, null: false
32-
add :stacktrace, :map, null: false
18+
create table(:error_tracker_meta,
19+
primary_key: [name: :key, type: :string],
20+
prefix: prefix
21+
) do
22+
add :value, :string, null: false
23+
end
3324

34-
add :error_id, references(:error_tracker_errors, on_delete: :delete_all, type: :bigserial),
35-
null: false
25+
create table(:error_tracker_errors,
26+
primary_key: [name: :id, type: :bigserial],
27+
prefix: prefix
28+
) do
29+
add :kind, :string, null: false
30+
add :reason, :text, null: false
31+
add :source_line, :text, null: false
32+
add :source_function, :text, null: false
33+
add :status, :string, null: false
34+
add :fingerprint, :string, null: false
35+
add :last_occurrence_at, :utc_datetime_usec, null: false
3636

37-
timestamps(type: :utc_datetime_usec, updated_at: false)
38-
end
37+
timestamps(type: :utc_datetime_usec)
38+
end
39+
40+
create unique_index(:error_tracker_errors, [:fingerprint], prefix: prefix)
41+
42+
create table(:error_tracker_occurrences,
43+
primary_key: [name: :id, type: :bigserial],
44+
prefix: prefix
45+
) do
46+
add :context, :map, null: false
47+
add :reason, :text, null: false
48+
add :stacktrace, :map, null: false
49+
50+
add :error_id,
51+
references(:error_tracker_errors, on_delete: :delete_all, type: :bigserial),
52+
null: false
53+
54+
timestamps(type: :utc_datetime_usec, updated_at: false)
55+
end
3956

40-
create index(:error_tracker_occurrences, [:error_id], prefix: prefix)
57+
create index(:error_tracker_occurrences, [:error_id], prefix: prefix)
58+
else
59+
:noop
60+
end
4161
end
4262

4363
def down(%{prefix: prefix}) do
4464
drop table(:error_tracker_occurrences, prefix: prefix)
4565
drop table(:error_tracker_errors, prefix: prefix)
66+
drop_if_exists table(:error_tracker_meta, prefix: prefix)
67+
end
68+
69+
def current_version_legacy(opts) do
70+
query =
71+
from pg_class in "pg_class",
72+
left_join: pg_description in "pg_description",
73+
on: pg_description.objoid == pg_class.oid,
74+
left_join: pg_namespace in "pg_namespace",
75+
on: pg_namespace.oid == pg_class.relnamespace,
76+
where: pg_class.relname == "error_tracker_errors",
77+
where: pg_namespace.nspname == ^opts.escaped_prefix,
78+
select: pg_description.description
79+
80+
case repo().one(query, log: false) do
81+
version when is_binary(version) -> String.to_integer(version)
82+
_other -> 0
83+
end
4684
end
4785
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
defmodule ErrorTracker.Migration.Postgres.V02 do
2+
@moduledoc false
3+
4+
use Ecto.Migration
5+
6+
def up(%{prefix: prefix}) do
7+
# For systems which executed versions without this migration they may not
8+
# have the error_tracker_meta table, so we need to create it conditionally
9+
# to avoid errors.
10+
create_if_not_exists table(:error_tracker_meta,
11+
primary_key: [name: :key, type: :string],
12+
prefix: prefix
13+
) do
14+
add :value, :string, null: false
15+
end
16+
17+
execute "COMMENT ON TABLE #{inspect(prefix)}.error_tracker_errors IS ''"
18+
end
19+
20+
def down(%{prefix: prefix}) do
21+
# We do not delete the `error_tracker_meta` table because it's creation and
22+
# deletion are controlled by V01 migration.
23+
execute "COMMENT ON TABLE #{inspect(prefix)}.error_tracker_errors IS '1'"
24+
end
25+
end

0 commit comments

Comments
 (0)