From 6ce8f052d3c827cc227c0a145cba6ef85a360bcd Mon Sep 17 00:00:00 2001 From: Nick Mohoric Date: Sun, 15 Oct 2017 22:52:58 -0400 Subject: [PATCH] Initial commit --- .gitignore | 5 ++ .travis.yml | 5 ++ LICENSE.txt | 21 +++++++++ README.md | 41 +++++++++++++++++ config/config.exs | 24 ++++++++++ lib/plug_opentracing.ex | 84 ++++++++++++++++++++++++++++++++++ mix.exs | 43 +++++++++++++++++ mix.lock | 4 ++ test/plug_opentracing_test.exs | 53 +++++++++++++++++++++ test/test_helper.exs | 28 ++++++++++++ 10 files changed, 308 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 config/config.exs create mode 100644 lib/plug_opentracing.ex create mode 100644 mix.exs create mode 100644 mix.lock create mode 100644 test/plug_opentracing_test.exs create mode 100644 test/test_helper.exs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..12d32ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/_build +/deps +erl_crash.dump +*.ez +/doc diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9b1923f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: elixir +elixir: + - 1.5.1 +otp_release: + - 20.0 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..c35425b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Nick Mohoric + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..eb98ef4 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# PlugOpenTracing + +[![Build Status](https://travis-ci.org/nmohoric/plug_opentracing.svg?branch=master)](https://travis-ci.org/nmohoric/plug_opentracing) + +An Elixir Plug for adding OpenTracing instrumentation. + +## Usage + +Update your `mix.exs` file and run `mix deps.get`. +```elixir +defp deps do + [{:plug_opentracing, "~> 0.1"}] +end +``` + +Add the plug to a pipeline. In this case we will look for the +`uber-trace-id` header to exist. If so, it is split on `:` and +the first and second values are used as the trace_id and parent_id +when creating the span. + +```elixir +defmodule MyPhoenixApp.MyController do + use MyPhoenixApp.Web, :controller + alias Plug.Conn.Status + + plug PlugOpenTracing, trace_id: 'uber-trace-id' + plug :action + + def index(conn, _params) do + conn + |> put_status(Status.code :ok) + end +end + +``` +The created span can be accessed using `conn.assigns[:trace_span]`. This is useful +when you want to use this span as the parent of another span in your request, or +if you want to add tags/logs to the span before it is finished. + +The request span will be finished at the end of your request using a callback, +which will send it to the Jaeger or Zipkin endpoint you've set up in your config. diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..6dfa82f --- /dev/null +++ b/config/config.exs @@ -0,0 +1,24 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for third- +# party users, it should be done in your mix.exs file. + +# Sample configuration: +# +# config :logger, :console, +# level: :info, +# format: "$date $time [$level] $metadata$message\n", +# metadata: [:user_id] + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/lib/plug_opentracing.ex b/lib/plug_opentracing.ex new file mode 100644 index 0000000..47d395d --- /dev/null +++ b/lib/plug_opentracing.ex @@ -0,0 +1,84 @@ +defmodule PlugOpenTracing do + @moduledoc """ + A plug for adding OpenTracing instrumentation to requests. + + If a request already has a trace-id, the parts will be split + out and added as trace-id and parent-id in the new span. Otherwise, + a new span will be created. + + The default header is "uber-trace-id" but this can be set at compile time. + + At any point in the request you can access the span using conn.assigns[:trace]. + This can then be used to either add tags onto it or use the span as a parent span + for other requests. + + To use it, just plug it into your module: + + plug PlugOpenTracing + + ## Options + + * `:trace_id` - An incoming trace-id. This should be hex and in the + form `trace-id:span-id` plus any optional bits that will be removed. + + plug PlugOpenTracing, trace_id: "my-trace-header" + """ + + alias Plug.Conn + alias :otter, as: Otter + @behaviour Plug + + def init(opts) do + Keyword.get(opts, :trace_header, "uber-trace-id") + end + + def call(conn, trace_id_header) do + conn + |> get_trace_id(trace_id_header) + |> start_span() + |> tag_span() + |> register_span() + end + + defp get_trace_id(conn, header) do + case Conn.get_req_header(conn, header) do + [] -> {conn, nil} + [val|_] -> {conn, extract_id(String.split(val, ":"))} + end + end + + defp start_span({conn, [nil, _]}), do: start_span({conn, nil}) + defp start_span({conn, [_, nil]}), do: start_span({conn, nil}) + defp start_span({conn, [trace_id, span_id]}) do + {conn, Otter.start(conn.method, trace_id, span_id)} + end + defp start_span({conn, _}), do: {conn, Otter.start(conn.method)} + + defp tag_span({conn, span}) do + span = span + |> Otter.tag("path", Enum.join(Enum.map(conn.path_info, &URI.decode/1), "/")) + |> Otter.tag("method", conn.method) + conn |> Conn.assign(:trace_span, span) + end + + defp register_span(conn) do + Conn.register_before_send(conn, fn c -> + c.assigns[:trace_span] + |> Otter.finish() + c + end) + end + + defp extract_id(vals) when length(vals) >= 2 do + vals + |> Enum.take(2) + |> Enum.map(fn(s) -> s + |> Integer.parse(16) + |> case do + :error -> nil + i -> elem(i,0) + end + end) + end + defp extract_id(_), do: nil +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..9f0dd91 --- /dev/null +++ b/mix.exs @@ -0,0 +1,43 @@ +defmodule PlugOpenTracing.Mixfile do + use Mix.Project + + def project do + [ + app: :plug_opentracing, + version: "0.0.1", + name: "PlugOpenTracing", + source_url: "https://github.com/nmohoric/plug_opentracing", + elixir: "~> 1.0", + deps: deps(), + description: description(), + package: package() + ] + end + + defp description do + """ + An Elixir Plug for adding opentracing instrumentation. + """ + end + + defp package do + [ + maintainers: ["Nick Mohoric"], + files: ["lib", "mix.exs", "README*", "LICENSE*"], + licenses: ["MIT"], + links: %{"GitHub" => "https://github.com/nmohoric/plug_opentracing"} + ] + end + + def application do + [applications: [:plug]] + end + + defp deps do + [ + {:plug, "~> 1.3"}, + {:ibrowse, "~> 4.4"}, + {:otter, "~> 0.2.0"}, + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..1da963d --- /dev/null +++ b/mix.lock @@ -0,0 +1,4 @@ +%{"ibrowse": {:hex, :ibrowse, "4.4.0", "2d923325efe0d2cb09b9c6a047b2835a5eda69d8a47ed6ff8bc03628b764e991", [], [], "hexpm"}, + "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, + "otter": {:hex, :otter, "0.2.0", "0ca08e94db43c9ef6bf22a9b803a776880841e9f7fbf7b9207ade0cdeb0c6251", [], [], "hexpm"}, + "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}} diff --git a/test/plug_opentracing_test.exs b/test/plug_opentracing_test.exs new file mode 100644 index 0000000..95e3fbe --- /dev/null +++ b/test/plug_opentracing_test.exs @@ -0,0 +1,53 @@ +defmodule PlugOpenTracingTest do + use ExUnit.Case, async: true + use Plug.Test + alias Plug.Conn.Status + + test "missing trace header results in new span" do + connection = conn(:get, "/test-path") + response = TestApp.call(connection, []) + + assert response.status == Status.code(:ok) + assert response.resp_body != "" + end + + test "bad trace header results in new span" do + connection = conn(:get, "/test-path") |> put_req_header("uber-trace-id", "1234") + response = TestApp.call(connection, []) + + assert response.status == Status.code(:ok) + assert response.resp_body != "" + end + + test "valid trace header results in span with those ids" do + connection = conn(:get, "/test-path") |> put_req_header("uber-trace-id", "6a5c63925e01051b:150b1b1adcde6f4:6a5c63925e01051b:1") + response = TestApp.call(connection, []) + + assert response.status == Status.code(:ok) + assert response.resp_body == "7664110146171241755" + end + + test "invalid hex trace header results in usable span" do + connection = conn(:get, "/test-path") |> put_req_header("uber-trace-id", "6a5x63925t01051b:150b1p1adcdq6f4:6a5c63925e01051b:1") + response = TestApp.call(connection, []) + + assert response.status == Status.code(:ok) + assert response.resp_body == "1701" + end + + test "empty trace header results in new span" do + connection = conn(:get, "/test-path") |> put_req_header("uber-trace-id", "") + response = TestApp.call(connection, []) + + assert response.status == Status.code(:ok) + assert response.resp_body != "" + end + + test "just some colons results in new span" do + connection = conn(:get, "/test-path") |> put_req_header("uber-trace-id", "::::") + response = TestApp.call(connection, []) + + assert response.status == Status.code(:ok) + assert response.resp_body != "" + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..bd46c18 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,28 @@ +ExUnit.start() + +defmodule AppMaker do + defmacro __using__(options) do + quote do + use Plug.Router + alias Plug.Conn.Status + + plug PlugOpenTracing, unquote(options) + plug :match + plug :dispatch + end + end +end + +defmodule TestApp do + alias :otter, as: Otter + use AppMaker, trace_header: "uber-trace-id" + + get "/test-path" do + send_resp(conn, Status.code(:ok), ids_to_string(conn)) + end + + defp ids_to_string(conn) do + {trace_id, _} = Otter.ids(conn.assigns[:trace_span]) + "#{Integer.to_string(trace_id)}" + end +end