Skip to content
/ ot Public

Opentracing support for Elixir applications

License

Notifications You must be signed in to change notification settings

sumup-oss/ot

Repository files navigation

Ot

License

Opentracing for elixir applications.

Plug into your app via just a few lines of code, orchestrate span trees and send them to Jaeger, Zipkin, NewRelic, or any other tracing system.

Prerequisites

  1. You will need a general understanding of opentracing's concepts. You can find a good starting point here
  2. Install Elixir v1.9 or higher
  3. This project depends on tesla. Check out their documentation to learn more about Tesla and how middlewares work there.
  4. Throughout the examples of this readme we have used excerpts of a sample Phoenix web app, it is a good idea to get familiar with it first.

Installation

Add this line to your mix.exs:

defp deps do
  [{:ot, "~> 3.0"}]
end

Then run mix deps.get and you are good to go!

Quick start

# See "Configuration" section
Ot.start_link(config)

# See "Module overview and functions" section
Ot.start_span("my-test-span", nil)  # <- span is initialized
Ot.tag("my-tag", "tag-value")       #
Ot.log("log message")               #
Ot.end_span()                       # <- span will be sent to jaeger

Example usage in a Phoenix app

In config/dev.exs:

# See "Configuration" section
config :my_app, :ot_config,
  service_name: "my-app",
  collectors: [
    jaeger: [
      url: "http://127.0.0.1:9411/api/v2/spans",
      adapter: Tesla.Adapter.Hackney,
      stringify_tags: true
    ]
  ]

In application.ex:

defmodule MyApp.Application do
  def start(_type, _args) do
    children = [
      {Ot, Application.fetch_env!(:my_app, :ot_config)}
      # ... other children

In endpoint.ex:

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # See "Plug" section
  plug Ot.Plug

  # ... other plugs

Configuration

Example with all supported configuration options:

# Tesla adapter to use in collectors (can be different for each collector)
# If you use this one, you must add :hackney to your deps
adapter = {Tesla.Adapter.Hackney, connect_timeout: 5000, recv_timeout: 5000}

ot_config = [
  service_name: "my-app",       # (required)
  ignored_exceptions: [         # (optional) don't set "error=true" for these
    Phoenix.Router.NoRouteError
  ],
  plug: [                                      # (optional) config for Ot.Plug
    paths: ["/v0.1/*"],                        # (optional) paths to work with; default: ["*"]
    response_body_tag: "http.response_body",   # (optional) tag resp body as "http.resp_body"; default: nil (no tag)
    conn_private_tags: [                       # (optional) list of conn-private fields to tag; default: []
      req_body: "http.request_body",           #    tag conn.private.req_body as "http.request_body"
      req_id: "http.request_id"                #    tag conn.private.req_id as "http.request_id"
    ]
  ],
  collectors: [
    jaeger: [
      url: "...",               # (required) span endpoint (Zipkin JSON v2 format)
      adapter: tesla_adapter,   # (required) tesla adapter to use
      flush_interval: 500,      # (optional) buffer flush interval in ms; default: 1000
      flush_retries: 1,         # (optional) flush retry attempts; default: 5 *
      stringify_tags: true,     # (optional) convert tag values to strings (required for jaeger)
      middlewares: []           # (optional) Tesla middlewares; default: []
    ],
    newrelic: [
      url: "https://trace-api.newrelic.com/trace/v1",
      adapter: tesla_adapter,
      middlewares: [
        Tesla.Middleware.Logger,
        {Tesla.Middleware.Headers, [
          {"api-key", "..."},
          {"data-format", "zipkin"},
          {"data-format-version", "2"}
        ]}
      ]
    ]
  ]
]

* tracing data is stored in a local buffer and sent to the tracing backends (jaeger, zipkin, etc.) in a batch, every X ms. On failure, the buffer is preserved and continues to accumulate spans till the next flush (retry). If the retry limit is exceeded, the buffer is discarded to prevent memory leaks.

Plug

Web applications can use Ot.Plug to automatically have a span created for each incoming request.

Let's take a simple ping-pong web app and try this simple request:

curl 'http://localhost:4000/v0.1/ping?player=just_me'

The controller:

def MyAppWeb.MyController do
  def ping(conn, params) do
    Ot.tag("opponent", params["player"])
    send_resp(conn, 200, "pong")
  end
end

will produce this span, sent to the collector(s):

{
  "annotations": [],
  "duration": 1375,
  "id": "d98f95a2d087a665",
  "kind": "SERVER",
  "localEndpoint":
  {
    "ipv4": "127.0.0.1",
    "port": 0,
    "serviceName": "my-app"
  },
  "name": "request",
  "parentId": null,
  "tags":
  {
    "component": "my-app",
    "author": "just_me",
    "http.method": "GET",
    "http.path": "/v0.1/ping",
    "http.query_string": "player=just_me",
    "http.status_code": "200"
  },
  "timestamp": 1628150108323753,
  "traceId": "0a922fbb7df193916b283610bcc516ff"
}

Note that the values of ipv4 and port are hard-coded (could be made configurable in a later release).

Module overview and functions

Check out the Module overview docs.

Contributing

Check out CONTRIBUTING.md

Code of conduct (CoC)

We want to foster an inclusive and friendly community around our Open Source efforts. Like all SumUp Open Source projects, this project follows the Contributor Covenant Code of Conduct. Please, read it and follow it.

If you feel another member of the community violated our CoC or you are experiencing problems participating in our community because of another individual's behavior, please get in touch with our maintainers. We will enforce the CoC.

About SumUp

SumUp logo

It is our mission to make easy and fast card payments a reality across the entire world. You can pay with SumUp in more than 30 countries, already. Our engineers work in Berlin, Cologne, Sofia and Sāo Paulo. They write code in JavaScript, Swift, Ruby, Go, Java, Erlang, Elixir and more. Want to come work with us? Head to our careers page to find out more.