Giter Site home page Giter Site logo

sibyl's Introduction

Sibyl

hex.pm hexdocs.pm hex.pm hex.pm

Easy, ergonomic telemetry & observability for Elixir applications.

Why

Sibyl aims to solve three main problems:

  1. It isn't always clear how best to emit telemetry events in your Elixir projects as :telemetry is rather low level, and a lot of examples focus on library code.
  2. Telemetry & observability is either too high level, or requires a lot of mainly instrumentation which can be noisy and be error-prone when done manually.
  3. Emitting events/telemetry and consuming them are seperated concerns. You're on your own for deciding how to consume the events you do emit in your code.

The above is actually great for building libraries where you want to emit events and allow external users to consume said events, and do so in an unopinionated way. However, applications tend to emit events explicitly so that they can be consumed, and tend to want to do so in an opinionated or constrained way.

Sibyl tries to solve the above by being a light wrapper around :telemetry and embracing OpenTelemetry.

Installation

Add :sibyl to your list of dependencies in mix.exs:

def deps do
  [
    {:sibyl, "~> 0.1.0"}
  ]
end

Currently, Sibyl requires Elixir 1.13 or higher. We aim to support Sibyl for the three most recent Elixir major releases at any given time.

Usage

Sibyl is an opinionated library that aims to get you tracing your code and emitting metrics with minimal instrumentation as quickly as possible!

Before actually emitting any metrics/events in your code, you need to configure Sibyl to automatically start up by adding the following to your project's Application module:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    ...
  after
    :ok = Sibyl.Handlers.attach_all_events(Sibyl.Handlers.OpenTelemetry)
  end
end

Tracing Functions

You can start tracing functions, capturing their runtime, return values, exceptions, and more by using the trace/0 macro with the @decorate directive which is provided to any module that has use Sibyl in it.

Traced functions automatically emit :telemetry events when they initially get called, when they end, and when they throw an exception. Sibyl will capture the time elapsed, arguments provided, return value, and any events (and their measurements, metadata) emitted during the function.

defmodule MyApp.Users do
  alias MyApp.User
  alias MyApp.Repo

  use Sibyl

  @decorate trace()
  def register(attrs) do
    attrs
    |> User.changeset()
    |> Repo.insert()
    |> case do
      {:ok, user} ->
        {:ok, user}

      {:error, reason} ->
        {:error, reason}
    end
  end
end

Tracing Modules

Typically, we recommend that functions be traced with purpose to minimize noise, however, Sibyl is able to automatically trace every function defined in a module that using the trace/0 macro with the @decorate_all directive instead of @decorate.

defmodule MyApp.Users do
  use Sibyl

  @decorate_all trace()

  def register(attrs) do
    attrs
    |> User.changeset()
    |> Repo.insert()
    |> case do
      {:ok, user} ->
        {:ok, user}

      {:error, reason} ->
        {:error, reason}
    end
  end
end

Emitting Events

Additionally, aside from tracing the runtime and state of your functions, Sibyl also makes it easy to emit arbitrary events and metrics in your application.

Unlike using the standard :telemetry library directly, Sibyl will ensure that any event being emitted was previously defined by Sibyl at compile time. This guarantees that events that are emitted exist, and makes your events durable across refactors and renaming.

You can define events with the define_event/1 macro which is automatically imported whenever you use Sibyl, and you can emit them via the emit/1 macro:

defmodule MyApp.Users do
  use Sibyl

  define_event(:registration)
  define_event(:registration_failed)

  def create_user(attrs) do
    attrs
    |> User.changeset()
    |> Repo.insert()
    |> case do
      {:ok, user} ->
        emit(:registration)
        {:ok, user}

      {:error, changeset} ->
        emit(:registration_failed)
        {:error, changeset}
    end
  end
end

Alternatively, events can be defined in other modules and emitted by referencing the definer such as:

defmodule MyApp.Events do
  use Sibyl

  define_event(:function_executed)
  define_event(:api_key_requests)
  define_event(:user_requests)
end

defmodule MyApp.Users do
  use Sibyl
  alias MyApp.Events

  def create_user(attrs) do
    emit(Events, :function_executed)
    if is_api_user(self())?, do: emit(Events, :api_key_requests),
                             else: emit(Events, :user_requests)

    attrs
    |> User.changeset()
    |> Repo.insert()
  end
end

Plugins

Because Sibyl builds on top of the de-facto telemetry library for the BEAM, it's able to provide an easy way to extend the events Sibyl is able to handle via first class plugins.

You can configure plugins on a handler by handler basis via the following configuration:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    ...
  after
    :ok = Sibyl.Handlers.attach_all_events(Sibyl.Handlers.OpenTelemetry, plugins: [
      Sibyl.Plugins.Absinthe,
      Sibyl.Plugins.Ecto,
      ...
    ])
  end
end

See the documentation for more information.

Runtime Tracing

Sibyl is additionally able to trace and handle :telemetry events entirely at runtime, with no orchestration needed at all!

This is done by leveraging the BEAM's built in trace/3 BIFs, mapping internal BEAM events to :telemetry alike event emissions.

Using Sibyl.Dynamic looks like the following:

iex> Sibyl.Dynamic.enable(Sibyl.Handlers.OpenTelemetry)
iex> Sibyl.Dynamic.trace(MyApp.Users, :create_user, 1)
iex> MyApp.Users.create_user(%{email: "test"}) # Emits Sibyl-compatible `:telemetry` events
{:ok, %MyApp.User{}}

Additional Features

See the documentation for more exhaustive information about Sibyl's features, but other features not covered by the above includes:

  • Open and extendable Sibyl.Handler behaviour for defining alternative handlers
  • Speedscope and Chrome compatible flamegraph handler via Sibyl.Handlers.FlameGraph
  • More soon!

Contributing

We enforce 100% code coverage and a strict linting setup for Sibyl.

Please ensure that commits pass CI. You should be able to run both mix test and mix lint locally.

See the mix.exs to see the breakdown of what these commands do.

Additionally, we develop Sibyl using tools to manage our Elixir versions such as asdf or nix. Please see .tool-versions or shell.nix accordingly.

License

See LICENSE.md

sibyl's People

Contributors

vereis avatar tomasz-tomczyk avatar robmckinnon avatar florius0 avatar

Stargazers

Sebastian YEPES avatar Alex Bruns avatar  avatar Simon Escobar Benitez avatar Kathy Rodante avatar Jarrod Ruhland avatar  avatar Steve avatar Troy Rosenberg avatar Robson Gian Perassoli avatar Marcell Guilherme Costa da Silva avatar Matt Lambie avatar Julio Cabrera avatar Steffen Deusch avatar Shahryar Tavakkoli avatar Dwi Prihandi avatar Michel Perez avatar  avatar KaFai avatar  avatar Iván Costa avatar Abesse Smahi avatar Niranjan Anandkumar avatar Hissssst avatar Dimitar Panayotov avatar Jakub Jasiulewicz avatar Gianni Vialetto avatar Brad Folkens avatar  avatar John Barker avatar Malian avatar mcade avatar Thibault Deutsch avatar Thomas Coopman avatar Antonio Schiavon avatar Ilja Krijger avatar Max Gorin avatar Juha avatar Arek Gil avatar Matheus Macabu avatar Benjamin Yu avatar Kyle Boe avatar Mike Zornek avatar Jinkyou Son avatar Roman Pushkov avatar Camilo avatar Rafał Studnicki avatar  avatar Forest avatar Phil  avatar Noah Betzen avatar Dairon M. avatar  avatar Edwin Tsatsu avatar Erfan Hanifezade avatar  avatar Ivy Markwell avatar Austin Ziegler avatar  avatar Sam Gaw avatar Filipe Varjão avatar Clay avatar  avatar

Watchers

Tim Chambers avatar  avatar Noah Betzen avatar Sam Ginn avatar  avatar

Forkers

florius0

sibyl's Issues

Implement a Plug which allows overriding of OpenTelemetry trace metadata

It would be convenient to be able to provide a simple plug that checks an incoming HTTP request for a particular (maybe configurable) header that contains metadata that we add to any traced functions or emitted events.

We need to take care to only compile and provide said plug if plug exists.

Don't allow emission of `list(atom())` events without specifying source module, and add compile time error

Right now Sibyl allows us to emit events via: Sibyl.emit(:event) and Sibyl.emit([:some, :other, :event]).

The former has compile-time checks that :event exists in the module trying to emit said event, but the latter has no checks at all.

We should make it such that Sibyl.emit([:some, :other, :event]) either:

  1. Checks the source module to determine if the event is defined if no module is given, or
  2. Forces you to call it via: Sibyl.emit(Module, [:some, :other, :event]) which allows us to add in the missing compile time checks.

Performance

Hi, great idea for a library, but I think that it is extremely inefficient to perform a check of every module in the system when emitting an event with Sibyl.emit (which calls Event.event? which calls reflect/0 which loads and calls every module in the system)

First, you'll definitely need a prefix tree (or at least a MapSet) to perform event? efficiently.
Second, I'd just generate a function like __sibyl_events__ which would return a list of events defined in the module, instead of looking into attributes (and attributes list can be huge)

Then, I can suggest doing one of 3 things:

  1. As a user, I don't want to load every event by default. So I'd prefer to call something like Sibyl.setup_events(list_of_apps or maybe list_of_modules or even list_of_events) which would store a list of all events in persisten_term, and I'd have to manually call it on recompile if I want to load all events.

  2. I don't think that I'd use a library which automatically loads all events from dependency, but if you think that this is the way to go, you should at least cache the result of reflect/0 (and cache it in some search-optimized form)

  3. Another solution is to setup a convention that every module emits an event with this name in prefix. For example, Project.Context.Something would emit [:sibyl, :project, :context, :something, ...]. This would make this check somewhat more efficient (but you'd have to concat this prefix into multiple modules, etc). This is a bad solution, but it is still better than checking every module in the system

Fix deprecation warnings under Elixir 1.15

Fix deprecation warnings under Elixir 1.15. The deprecation warnings are:

==> sibyl
Compiling 13 files (.ex)
warning: Logger.warn/1 is deprecated. Use Logger.warning/2 instead
  (sibyl 0.1.8) lib/sibyl/handlers.ex:57: Sibyl.Handlers.ensure_name/1

warning: :dbg.stop_clear/0 is deprecated. It will be removed in OTP 27. Use dbg:stop/0 instead
Invalid call found at 2 locations:
  (sibyl 0.1.8) lib/sibyl/dynamic.ex:33: Sibyl.Dynamic.enable/1
  (sibyl 0.1.8) lib/sibyl/dynamic.ex:49: Sibyl.Dynamic.disable/0

warning: Exception.exception?/1 is deprecated. Use Kernel.is_exception/1 instead
  (sibyl 0.1.8) lib/sibyl/handlers/open_telemetry.ex:134: Sibyl.Handlers.OpenTelemetry.do_handle_event/4

ArgumentError in `Sibyl.Handlers.attach_all_events/2` in docker

I was evaluating Sybil and came across this error:

** (Mix) Could not start application fish_finder: exited in: FF.Application.start(:normal, [])
     ** (EXIT) an exception was raised:
         ** (ArgumentError) errors were found at the given arguments:

   * 1st argument: invalid destination

             :erlang.send(:undefined, {:shell_state, #PID<0.3858.0>})
             (stdlib 5.0.1) shell.erl:590: :shell.get_state/0
             (stdlib 5.0.1) shell.erl:597: :shell.get_function/2
             (stdlib 5.0.1) shell_default.erl:159: :shell_default."$handle_undefined_function"/2
             (sibyl 0.1.2) lib/sibyl/events.ex:105: Sibyl.Events.reflect/1
             (elixir 1.15.0) lib/enum.ex:4317: Enum.flat_map_list/2
             (elixir 1.15.0) lib/enum.ex:4318: Enum.flat_map_list/2
             (sibyl 0.1.2) lib/sibyl/handlers.ex:20: Sibyl.Handlers.attach_all_events/2
             (fish_finder 0.1.0) lib/fish_finder/application.ex:29: FF.Application.start/2
             (kernel 9.0.1) application_master.erl:293: :application_master.start_it_old/4

It was not reproduceable on my local elixir, but only in docker.

My repo using which the bug may be reproduced.

From what I'm gathering, :shell.__info__ results not in exception, but in message sending to :undefied pid.

Versions

Local:

  • OS: macOS 13.4
  • Erlang/Elixir:
Erlang/OTP 25 [erts-13.2.2.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [jit:ns] [dtrace]

Elixir 1.15.0 (compiled with Erlang/OTP 25)

Docker:

  • Runtime: Orbstack 0.16.1
  • bitwalker/alpine-elixir-phoenix:1.15.0

Use `DynamicSupervisor` and `GenServer`s for `Sibyl.Dynamic`

See TODO linked in Sibyl.Dynamic module for more info, but in short:

Right now, dynamic tracing traces all specified functions via one single process. This is fine for simple traces, but unless we distinguish between invocations of specified functions by their PIDs, we risk accidentally starting, nesting, or ending spans prematurely.

We should instead, based on the PID of the incoming dynamic trace, delegate the handling of messages to GenServers under a DynamicSupervisor which can start up if needed.

Do not emit warnings for underscored arguments

When using @decorate trace(), warnings for underscored arguments are emitted, however these arguments are not being used in the function itself:

# ...
  @decorate trace()
  def index(conn, _params) do
    small_fish = find_fish()
    large_fish = find_fish("large")

    json(conn, small_fish ++ large_fish)
  end
# ...

Will result in warning: the underscored variable "_params" is used after being set. A leading underscore indicates that the value of the variable should be ignored. If this is intended please rename the variable to remove the underscore

Probably can be fixed with generated: true in quotes

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.