DEV Community 👩‍💻👨‍💻

vKxni
vKxni

Posted on

Elixir Telemetry Logging

Ever wondered how you can log your Ecto/Mix Events to a file or even to the console? Well, here is how!

A small showcase

Preview, click here!

⚠️ Warning:
This isn't valid SQL code, this is just logging the Code being executed by Ecto that is able to save/edit/delete/purge data.

Requirements:

  • Elixir v13.3.2 +
  • A working Database
  • Basic Knowledge of Elixir
$ elixir -v
Elixir 1.13.2 (compiled with Erlang/OTP 24)
Enter fullscreen mode Exit fullscreen mode

Getting started

Here, as an example, I am using Ecto - however, you can use basically everything else (that might be similar) and directly skip to the Telemetry part.

Docs: https://hexdocs.pm/ecto/getting-started.html

Creating our project

$ mix new sqlt --sup
Enter fullscreen mode Exit fullscreen mode

Adding packages

If you use another DB, like MySQL, then add that instead.

{:ecto_sql, "~> 3.0"},
{:postgrex, ">= 0.0.0"}
Enter fullscreen mode Exit fullscreen mode

make sure to run mix deps.get to install them.

Generating our Repo

$ mix ecto.gen.repo -r SQLT.Repo
Enter fullscreen mode Exit fullscreen mode

This command will generate the configuration required to connect to a database. The first bit of configuration is in config/config.exs:

config :sqlt, SQLT.Repo,
  database: "localhost",
  username: "root",
  password: "admin",
  hostname: "localhost"
Enter fullscreen mode Exit fullscreen mode

Make sure to fill out the correct informations here.

NOTE: Your PostgreSQL database may be setup to

  • not require a username and password. If the above configuration doesn't work, try removing the username and password fields, or setting them both to "postgres".
  • be running on a non-standard port. The default port is 5432. You can specify your specific port by adding it to the config: e.g. port: 15432.

Configurating our Repo

The SQLT.Repo module is defined in lib/sqlt/repo.ex by our mix ecto.gen.repo command:

defmodule SQLT.Repo do
  use Ecto.Repo,
    otp_app: :sqlt,
    adapter: Ecto.Adapters.Postgres
end
Enter fullscreen mode Exit fullscreen mode

We now have to add our Repo to our Application. For that being said, open your application.ex file.

Possible path: lib/sqlt/application.ex.
Within the children, add the following:

 children = [
  # add this
  {SQLT.Repo, []},
]
Enter fullscreen mode Exit fullscreen mode

This piece of configuration will start the Ecto process which receives and executes our application's queries. Without it, we wouldn't be able to query the database at all!

There's one final bit of configuration that we'll need to add ourselves, since the generator does not add it. Underneath the configuration in config/config.exs, add this line:
config :sqlt, ecto_repos: [SQLT.Repo]

This tells our application about the repo, which will allow us to run commands such as mix ecto.create very soon.

Database Setup

Run the following command:

$ mix ecto.create
Enter fullscreen mode Exit fullscreen mode

You should now see
The database for SQLT.Repo has been created.

NOTE: If you get an error, you should try changing your configuration in config/config.exs, as it may be an authentication error.

This command will generate a brand new migration file in priv/repo/migrations, which is empty by default:

defmodule SQLT.Repo.Migrations.People do
  use Ecto.Migration

  def change do

end
Enter fullscreen mode Exit fullscreen mode

We will now create a table called people and also add some values to it.

defmodule SQLT.Repo.Migrations.People do
  use Ecto.Migration

  def change do
    create table(:people) do 
      add :first_name, :string
      add :last_name, :string
      add :age, :integer
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

To run this migration and create the people table in our database, we will run this command:

$ mix ecto.migrate
Enter fullscreen mode Exit fullscreen mode

Creating a schema

Let's create the schema within our application at lib/sqlt/people.ex:

defmodule SQLT.People do
  use Ecto.Schema

  @moduledoc """
  Changesets allow filtering, casting, validation and definition of constraints when manipulating structs.
  There is an example of working with changesets in the introductory documentation in the Ecto module. 
  The functions cast/4 and change/2 are the usual entry points for creating changesets. 
  The first one is used to cast and validate external parameters, such as parameters sent through a 
  form, API, command line, etc. 
  The second one is used to change data directly from your application.
    - https://hexdocs.pm/ecto/Ecto.Changeset.html
  The remaining functions in this module, such as validations, constraints, 
  association handling, are about manipulating changesets. 
  """

  def changeset(person, params \\ %{}) do
    person
    # tell what parameters are allowed to be passted through 
    |> Ecto.Changeset.cast(params, [:first_name, :last_name, :age])
    # we expect values for fn + ln
    |> Ecto.Changeset.validate_required([:first_name, :last_name])
  end

  schema "people" do
    field(:first_name, :string)
    field(:last_name, :string)
    field(:age, :integer)
  end
end
Enter fullscreen mode Exit fullscreen mode

Awesome! Our database is now fully working.


Adding telemetry

Telemetry is a lightweight library for dynamic dispatching of events, with a focus on metrics and instrumentation. Any Erlang or Elixir library can use telemetry to emit events. Application code and other libraries can then hook into those events and run custom handlers.

Docs: https://hexdocs.pm/telemetry/readme.html

Configurating our Application

Open your application.ex file (lib/sqlt/application)
and add the following line

 :ok = :telemetry.attach("sqlt-repo-handler", [:sqlt, :repo, :query], &SQLT.Telemetry.handle_event/4, %{})
Enter fullscreen mode Exit fullscreen mode

above the children.

Your application.ex should now look like this:

defmodule Sqlt.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
   # here!! :)
    :ok = :telemetry.attach("sqlt-repo-handler", [:sqlt, :repo, :query], &SQLT.Telemetry.handle_event/4, %{})

    children = [
      {SQLT.Repo, []},
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Sqlt.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
Enter fullscreen mode Exit fullscreen mode

Configurating Telemetry

Start by creating a telemetry.ex file at the following path: lib/sqlt/telemetry.ex

defmodule SQLT.Telemetry do
  require Logger

def emit(value) do 
    :telemetry.execute([:sqlt, :emit], %{value: value})
end

# *
def handle_event([:sqlt, :repo, :query], measurements, metadata, _config) do
    data = "#{metadata.query} #{metadata.params}"

    Logger.info(data)
    File.write("raw.sql", data)
  end
end
Enter fullscreen mode Exit fullscreen mode

*
Here we handle "main event" for the telemetry handler defined in the application.ex file

:ok = :telemetry.attach("sqlt-repo-handler", [:sqlt, :repo, :query], &SQLT.Telemetry.handle_event/4, %{})

Once some event is executed, we log it to the console and also write the content into a separate file in our root directory.

We use the metadata variable with the query and params parameter, tho there are some more

:type - the type of the Ecto query. For example, for Ecto.SQL databases, it would be :ecto_sql_query

:repo - the Ecto repository

:result - the query result

:params - the query parameters

:query - the query sent to the database as a string

:source - the source the query was made on (may be nil)

:options - extra options given to the repo operation under :telemetry_options

Also, if you go through the code, you notice the handle_event function, this is where all the magic happens. Obviously you can remove the Logger (nothing will be logged to the console) and also change the behaviour (in my case writing the result to a .sql file).

Creating Events

In my case I am using Ecto Events, of course you can use something else.

I will now create some functions that is inserts, edits and also deletes data from the Database.

lib/sqlt/queries.ex

defmodule Query do
  require Ecto.Query

# here we create / insert data
def insert do
    firstname = IO.gets("Enter the firstname: ") |> String.trim()
    lastname = IO.gets("Enter the lastname: ") |> String.trim()
    person = %SQLT.People{first_name: firstname, last_name: lastname, age: 25}
    SQLT.Repo.insert(person)
  end

# here we edit our data
def edit do
    name = IO.gets("Enter the first_name that you want to find: ") |> String.trim()
    newname = IO.gets("Now enter the new first_name: ") |> String.trim()
    person = SQLT.People |> Ecto.Query.where(first_name: ^name) |> SQLT.Repo.one()
    changeset = SQLT.People.changeset(person, %{first_name: newname})

    case SQLT.Repo.update(changeset) do
      {:ok, person} ->
        IO.puts("Updated #{person}")

      {:error, error} ->
        IO.puts("Error #{error}")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

🎉🎉🎉 YOU ARE AWESOME! 🎉🎉🎉
You have successfully created a custom event logger with Telemetry.

If you run one of the functions above, you will see the raw.sql being created. By running more functions, telemetry will be write them into that file.

I have uploaded the whole code to GitHub, just in case if you want to take a look at the whole code/repo.

https://github.com/vKxni/sqlt

Top comments (0)

Google Cloud Superstar

Use your favorite Google Cloud APIs with MongoDB Atlas to build a highly-scalable modern application for your entry in the DEV x MongoDB Atlas Hackathon 2022.

Get Started