Skip to content
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
27 changes: 3 additions & 24 deletions dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Application.put_env(:error_tracker, ErrorTrackerDevWeb.Endpoint,
# Setup up the ErrorTracker configuration
Application.put_env(:error_tracker, :repo, ErrorTrackerDev.Repo)
Application.put_env(:error_tracker, :application, :error_tracker_dev)
Application.put_env(:error_tracker, :prefix, "private")

defmodule ErrorTrackerDevWeb.PageController do
import Plug.Conn
Expand Down Expand Up @@ -112,30 +113,8 @@ end
defmodule Migration0 do
use Ecto.Migration

def change do
create table(:error_tracker_errors) do
add :kind, :string, null: false
add :reason, :text, null: false
add :source_line, :text, null: false
add :source_function, :text, null: false
add :status, :string, null: false
add :fingerprint, :string, null: false

timestamps()
end

create unique_index(:error_tracker_errors, :fingerprint)

create table(:error_tracker_occurrences) do
add :context, :map, null: false
add :stacktrace, :map, null: false
add :error_id, references(:error_tracker_errors, on_delete: :delete_all), null: false

timestamps(updated_at: false)
end

create index(:error_tracker_occurrences, :error_id)
end
def up, do: ErrorTracker.Migrations.up(prefix: "private")
def down, do: ErrorTracker.Migrations.down(prefix: "private")
end

Application.put_env(:phoenix, :serve_endpoints, true)
Expand Down
9 changes: 7 additions & 2 deletions lib/error_tracker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ defmodule ErrorTracker do
error =
repo().insert!(error,
on_conflict: [set: [status: :unresolved]],
conflict_target: :fingerprint
conflict_target: :fingerprint,
prefix: prefix()
)

error
|> Ecto.build_assoc(:occurrences, stacktrace: stacktrace, context: context)
|> repo().insert!()
|> repo().insert!(prefix: prefix())
end

def repo do
Application.fetch_env!(:error_tracker, :repo)
end

def prefix do
Application.get_env(:error_tracker, :prefix, "public")
end
end
124 changes: 124 additions & 0 deletions lib/error_tracker/migrations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
defmodule ErrorTracker.Migrations do
@moduledoc """
Create and modify the database tables for the ErrorTracker.

## Usage

To use the ErrorTracker migrations in your application you will need to generate
a regular `Ecto.Migration` that performs the relevant calls to `ErrorTracker.Migration`.

```bash
mix ecto.gen.migration add_error_tracker
```

Open the generated migration file and call the `up` and `down` functions on
`ErrorTracker.Migration`.

```elixir
defmodule MyApp.Repo.Migrations.AddErrorTracker do
use Ecto.Migration

def up, do: ErrorTracker.Migrations.up()
def down, do: ErrorTracker.Migrations.down()
end
```

This will run every ErrorTracker migration for your database. You can now run the migration
and perform the database changes:

```bash
mix ecto.migrate
```

As new versions of the ErrorTracker are released you may need to run additional migrations.
To do this you can follow the previous process and create a new migration:

```bash
mix ecto.gen.migration update_error_tracker_to_vN
```

Open the generated migration file and call the `up` and `down` functions on the
`ErrorTracker.Migration` passing the desired `version`.

```elixir
defmodule MyApp.Repo.Migrations.UpdateErrorTrackerToVN do
use Ecto.Migration

def up, do: ErrorTracker.Migrations.up(version: N)
def down, do: ErrorTracker.Migrations.down(version: N)
end
```

Then run the migrations to perform the database changes:

```bash
mix ecto.migrate
```

## Custom prefix

ErrorTracker supports namespacing its own tables using PostgreSQL schemas, also known
as "prefixes" in Ecto. With prefixes your error tables can reside outside of your primary
schema (which is usually named "public").

To use a prefix you need to specify it in your migrations:

```elixir
defmodule MyApp.Repo.Migrations.AddErrorTracker do
use Ecto.Migration

def up, do: ErrorTracker.Migrations.up(prefix: "custom_schema")
def down, do: ErrorTracker.Migrations.down(prefix: "custom_schema")
end
```

This will automatically create the database schema for you. If the schema does already exist
the migration may fail when trying to recreate it. In such cases you can instruct the ErrorTracker
not to create the schema again:

```elixir
defmodule MyApp.Repo.Migrations.AddErrorTracker do
use Ecto.Migration

def up, do: ErrorTracker.Migrations.up(prefix: "custom_schema", create_schema: false)
def down, do: ErrorTracker.Migrations.down(prefix: "custom_schema")
end
```

If you are using a custom schema other than the default "public" you need to configure the
ErrorTracker to use that schema:

```elixir
config :error_tracker, :prefix, "custom_schema"
```
"""
defdelegate up(opts \\ []), to: ErrorTracker.Migration
defdelegate down(opts \\ []), to: ErrorTracker.Migration
end

defmodule ErrorTracker.Migration do
@moduledoc false

@callback up(Keyword.t()) :: :ok
@callback down(Keyword.t()) :: :ok
@callback current_version(Keyword.t()) :: non_neg_integer()

def up(opts \\ []) when is_list(opts) do
migrator().up(opts)
end

def down(opts \\ []) when is_list(opts) do
migrator().down(opts)
end

def migrated_version(opts \\ []) when is_list(opts) do
migrator().migrated_version(opts)
end

defp migrator do
case ErrorTracker.repo().__adapter__() do
Ecto.Adapters.Postgres -> ErrorTracker.Migrations.Postgres
adapter -> raise "ErrorTracker does not support #{adapter}"
end
end
end
88 changes: 88 additions & 0 deletions lib/error_tracker/migrations/postgres.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
defmodule ErrorTracker.Migrations.Postgres do
@behaviour ErrorTracker.Migration

use Ecto.Migration

import Ecto.Query

@initial_version 1
@current_version 1
@default_prefix "public"

@impl ErrorTracker.Migration
def up(opts) do
opts = with_defaults(opts, @current_version)
initial = current_version(opts)

cond do
initial == 0 ->
change(@initial_version..opts.version, :up, opts)

initial < opts.version ->
change((initial + 1)..opts.version, :up, opts)

true ->
:ok
end
end

@impl ErrorTracker.Migration
def down(opts) do
opts = with_defaults(opts, @initial_version)
initial = max(current_version(opts), @initial_version)

if initial >= opts.version do
change(initial..opts.version, :down, opts)
end
end

@impl ErrorTracker.Migration
def current_version(opts) do
opts = with_defaults(opts, @initial_version)
repo = Map.get_lazy(opts, :repo, fn -> repo() end)

query =
from pg_class in "pg_class",
left_join: pg_description in "pg_description",
on: pg_description.objoid == pg_class.oid,
left_join: pg_namespace in "pg_namespace",
on: pg_namespace.oid == pg_class.relnamespace,
where: pg_class.relname == "error_tracker_errors",
where: pg_namespace.nspname == ^opts.escaped_prefix,
select: pg_description.description

case repo.one(query, log: false) do
version when is_binary(version) -> String.to_integer(version)
_other -> 0
end
end

defp change(versions_range, direction, opts) do
for version <- versions_range do
padded_version = String.pad_leading(to_string(version), 2, "0")

migration_module = Module.concat(__MODULE__, "V#{padded_version}")
apply(migration_module, direction, [opts])
end

case direction do
:up -> record_version(opts, Enum.max(versions_range))
:down -> record_version(opts, Enum.min(versions_range) - 1)
end
end

defp record_version(%{prefix: prefix}, version) do
case version do
0 -> :ok
_other -> execute "COMMENT ON TABLE #{inspect(prefix)}.error_tracker_errors IS '#{version}'"
end
end

defp with_defaults(opts, version) do
opts = Enum.into(opts, %{prefix: @default_prefix, version: version})

opts
|> Map.put_new(:create_schema, opts.prefix != @default_prefix)
|> Map.put_new(:escaped_prefix, String.replace(opts.prefix, "'", "\\'"))
end
end
35 changes: 35 additions & 0 deletions lib/error_tracker/migrations/postgres/v01.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
defmodule ErrorTracker.Migrations.Postgres.V01 do
use Ecto.Migration

def up(%{create_schema: create_schema, prefix: prefix}) do
if create_schema, do: execute("CREATE SCHEMA IF NOT EXISTS #{prefix}")

create table(:error_tracker_errors, prefix: prefix) do
add :kind, :string, null: false
add :reason, :text, null: false
add :source_line, :text, null: false
add :source_function, :text, null: false
add :status, :string, null: false
add :fingerprint, :string, null: false

timestamps(type: :utc_datetime_usec)
end

create unique_index(:error_tracker_errors, [:fingerprint], prefix: prefix)

create table(:error_tracker_occurrences, prefix: prefix) do
add :context, :map, null: false
add :stacktrace, :map, null: false
add :error_id, references(:error_tracker_errors, on_delete: :delete_all), null: false

timestamps(type: :utc_datetime_usec, updated_at: false)
end

create index(:error_tracker_occurrences, [:error_id], prefix: prefix)
end

def down(%{prefix: prefix}) do
drop table(:error_tracker_occurrences, prefix: prefix)
drop table(:error_tracker_errors, prefix: prefix)
end
end
2 changes: 1 addition & 1 deletion lib/error_tracker/models/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule ErrorTracker.Error do

has_many :occurrences, ErrorTracker.Occurrence

timestamps()
timestamps(type: :utc_datetime_usec)
end

def new(exception, stacktrace = %ErrorTracker.Stacktrace{}) do
Expand Down
2 changes: 1 addition & 1 deletion lib/error_tracker/models/occurrence.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ defmodule ErrorTracker.Occurrence do
embeds_one :stacktrace, ErrorTracker.Stacktrace
belongs_to :error, ErrorTracker.Error

timestamps(updated_at: false)
timestamps(type: :utc_datetime_usec, updated_at: false)
end
end