├── test ├── test_helper.exs ├── support │ └── server_helper.ex └── bandit │ └── opentelemetry_bandit_test.exs ├── .formatter.exs ├── config ├── config.exs └── test.exs ├── CHANGELOG.md ├── .gitignore ├── README.md ├── mix.exs ├── lib └── opentelemetry_bandit.ex ├── LICENSE └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | try do 4 | import_config "#{Mix.env()}.exs" 5 | rescue 6 | _ -> :ok 7 | end 8 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :opentelemetry, 4 | processors: [{:otel_simple_processor, %{}}] 5 | 6 | config :logger, level: :error 7 | -------------------------------------------------------------------------------- /test/support/server_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule ServerHelper do 2 | defmacro __using__(_) do 3 | quote location: :keep do 4 | import Plug.Conn 5 | 6 | def init(opts) do 7 | opts 8 | end 9 | 10 | def call(conn, []) do 11 | function = String.to_atom(List.first(conn.path_info)) 12 | apply(__MODULE__, function, [conn]) 13 | end 14 | 15 | defoverridable init: 1, call: 2 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.4] - 2023-12-14 4 | ### Changed 5 | - Prepare to the public release 6 | 7 | ## [0.1.3] - 2023-12-07 8 | ### Changed 9 | - Fix bug `Reason={:badkey, :send_binary_frame_bytes...}` on `:websocket_stop` event 10 | 11 | ## [0.1.2] - 2023-10-26 12 | ### Changed 13 | - Update docs 14 | - Cleanup span context, after span ended 15 | - Bump deps 16 | 17 | ## [0.1.1] - 2023-10-24 18 | ### Changed 19 | - Update bandit to the stable 1.0 version 20 | - Improve test coverage 21 | - Update examples 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | *.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpentelemetryBandit 2 | 3 | Telemetry handler that creates Opentelemetry spans from [Bandit events](https://hexdocs.pm/bandit/Bandit.Telemetry.html#content). 4 | 5 | After installing, setup the handler in your application behaviour before your top-level supervisor starts. 6 | 7 | ```elixir 8 | OpentelemetryBandit.setup() 9 | ``` 10 | 11 | When phoenix is used, setup telemetry this way: 12 | 13 | ```elixir 14 | OpentelemetryBandit.setup() 15 | OpentelemetryPhoenix.setup(adapter: :bandit) 16 | ``` 17 | 18 | *Note:* Please, vote for the PR to opentelemetry-phoenix: https://github.com/open-telemetry/opentelemetry-erlang-contrib/pull/249 19 | 20 | ## Installation 21 | 22 | ```elixir 23 | def deps do 24 | [ 25 | {:opentelemetry_bandit, "~> 0.1.4"} 26 | ] 27 | end 28 | ``` 29 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule OpentelemetryBandit.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :opentelemetry_bandit, 7 | version: "0.1.4", 8 | elixir: "~> 1.14", 9 | aliases: aliases(), 10 | start_permanent: Mix.env() == :prod, 11 | description: description(), 12 | package: package(), 13 | elixirc_paths: elixirc_path(Mix.env()), 14 | deps: deps(), 15 | test_coverage: [tool: ExCoveralls], 16 | preferred_cli_env: [ 17 | coveralls: :test, 18 | "coveralls.detail": :test, 19 | "coveralls.post": :test, 20 | "coveralls.html": :test, 21 | "coveralls.cobertura": :test 22 | ], 23 | docs: [ 24 | main: "readme", 25 | extras: ["README.md"] 26 | ] 27 | ] 28 | end 29 | 30 | # Run "mix help compile.app" to learn about applications. 31 | def application do 32 | [ 33 | extra_applications: [:logger] 34 | ] 35 | end 36 | 37 | defp description do 38 | """ 39 | Telemetry handler that creates Opentelemetry spans from Bandit events. 40 | """ 41 | end 42 | 43 | defp package do 44 | [ 45 | files: ~w(lib .formatter.exs mix.exs LICENSE* README* CHANGELOG*), 46 | maintainers: ["Artem Solomatin"], 47 | links: %{ 48 | "GitHub" => "https://github.com/samokat-oss/opentelemetry-bandit" 49 | }, 50 | licenses: ["Apache-2.0"] 51 | ] 52 | end 53 | 54 | defp elixirc_path(:test), do: ["lib/", "test/support"] 55 | defp elixirc_path(_), do: ["lib/"] 56 | 57 | # Run "mix help deps" to learn about dependencies. 58 | defp deps do 59 | [ 60 | {:opentelemetry_api, "~> 1.2"}, 61 | {:opentelemetry_semantic_conventions, "~> 0.2"}, 62 | {:opentelemetry_telemetry, "~> 1.0"}, 63 | {:plug, "~> 1.15"}, 64 | {:telemetry, "~> 1.2"}, 65 | 66 | # dev dependencies 67 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 68 | {:excoveralls, "~> 0.18", only: :test}, 69 | {:bandit, "~> 1.0", only: [:dev, :test], runtime: false}, 70 | {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, 71 | {:opentelemetry, "~> 1.0", only: [:dev, :test]}, 72 | {:opentelemetry_exporter, "~> 1.0", only: [:dev, :test]}, 73 | {:req, "~> 0.3", only: [:dev, :test]} 74 | ] 75 | end 76 | 77 | defp aliases do 78 | ["test.coverage": ["coveralls.cobertura"]] 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/opentelemetry_bandit.ex: -------------------------------------------------------------------------------- 1 | defmodule OpentelemetryBandit do 2 | @moduledoc """ 3 | OpentelemetryBandit uses [telemetry](https://hexdocs.pm/telemetry/) handlers to create `OpenTelemetry` spans. 4 | 5 | Supported: 6 | 1. :bandit, :request, :stop 7 | 2. :bandit, :request, :exception 8 | 3. :bandit, :websocket, :stop 9 | """ 10 | 11 | alias OpenTelemetry.SemanticConventions.Trace 12 | require Trace 13 | require OpenTelemetry.Tracer 14 | 15 | @doc """ 16 | Initializes and configures the telemetry handlers. 17 | """ 18 | @spec setup(any) :: :ok 19 | def setup(_opts \\ []) do 20 | :telemetry.attach( 21 | {__MODULE__, :request_stop}, 22 | [:bandit, :request, :stop], 23 | &__MODULE__.handle_request_stop/4, 24 | %{} 25 | ) 26 | 27 | :telemetry.attach( 28 | {__MODULE__, :request_exception}, 29 | [:bandit, :request, :exception], 30 | &__MODULE__.handle_request_exception/4, 31 | %{} 32 | ) 33 | 34 | :telemetry.attach( 35 | {__MODULE__, :websocket_stop}, 36 | [:bandit, :websocket, :stop], 37 | &__MODULE__.handle_websocket_stop/4, 38 | %{} 39 | ) 40 | end 41 | 42 | def handle_request_stop(_event, measurements, meta, _config) do 43 | conn = Map.get(meta, :conn) 44 | duration = measurements.duration 45 | end_time = :opentelemetry.timestamp() 46 | start_time = end_time - duration 47 | 48 | url = extract_url(meta, conn) 49 | request_path = extract_request_path(meta, conn) 50 | 51 | attributes = 52 | if Map.has_key?(meta, :error) do 53 | %{ 54 | Trace.http_url() => url, 55 | Trace.http_method() => meta.method, 56 | Trace.net_transport() => :"IP.TCP", 57 | Trace.http_response_content_length() => measurements.resp_body_bytes, 58 | Trace.http_status_code() => meta.status 59 | } 60 | else 61 | %{ 62 | Trace.http_url() => url, 63 | Trace.http_client_ip() => client_ip(conn), 64 | Trace.http_scheme() => conn.scheme, 65 | Trace.net_peer_name() => conn.host, 66 | Trace.net_peer_port() => conn.port, 67 | Trace.http_target() => conn.request_path, 68 | Trace.http_method() => meta.method, 69 | Trace.http_status_code() => meta.status, 70 | Trace.http_response_content_length() => measurements.resp_body_bytes, 71 | Trace.net_transport() => :"IP.TCP", 72 | Trace.http_user_agent() => user_agent(conn) 73 | } 74 | end 75 | 76 | span_kind = if Map.has_key?(meta, :error), do: :error, else: :server 77 | 78 | span_id = "HTTP #{meta.method} #{request_path}" |> String.trim() 79 | 80 | OpenTelemetry.Tracer.start_span(span_id, %{ 81 | attributes: attributes, 82 | start_time: start_time, 83 | end_time: end_time, 84 | kind: span_kind 85 | }) 86 | |> set_span_status(meta, Map.get(meta, :error, "")) 87 | |> OpenTelemetry.Span.end_span() 88 | 89 | OpenTelemetry.Ctx.clear() 90 | end 91 | 92 | def handle_request_exception(_event, _measurements, meta, _config) do 93 | OpenTelemetry.Tracer.start_span("HTTP exception #{inspect(meta.exception.__struct__)}", %{ 94 | kind: :error, 95 | status: :error 96 | }) 97 | |> set_span_status(meta, inspect(meta.stacktrace)) 98 | |> OpenTelemetry.Span.end_span() 99 | 100 | OpenTelemetry.Ctx.clear() 101 | end 102 | 103 | def handle_websocket_stop(_event, measurements, meta, _config) do 104 | duration = measurements.duration 105 | end_time = :opentelemetry.timestamp() 106 | start_time = end_time - duration 107 | 108 | attributes = %{ 109 | :"websocket.recv.binary.frame.bytes" => Map.get(measurements, :send_binary_frame_bytes, 0), 110 | :"websocket.send.binary.frame.bytes" => Map.get(measurements, :recv_binary_frame_bytes, 0), 111 | Trace.net_transport() => :websocket 112 | } 113 | 114 | span_kind = if Map.has_key?(meta, :error), do: :error, else: :server 115 | 116 | OpenTelemetry.Tracer.start_span("Websocket", %{ 117 | attributes: attributes, 118 | start_time: start_time, 119 | end_time: end_time, 120 | kind: span_kind 121 | }) 122 | |> set_span_status(meta, Map.get(meta, :error, "")) 123 | |> OpenTelemetry.Span.end_span() 124 | 125 | OpenTelemetry.Ctx.clear() 126 | end 127 | 128 | defp set_span_status(span, meta, message) do 129 | status = if Map.has_key?(meta, :error) || message != "", do: :error, else: :ok 130 | 131 | OpenTelemetry.Span.set_status(span, OpenTelemetry.status(status, message)) 132 | span 133 | end 134 | 135 | defp extract_url(%{error: _} = meta, _conn) do 136 | case Map.get(meta, :request_target) do 137 | nil -> "" 138 | {scheme, host, port, path} -> build_url(scheme, host, port, path) 139 | end 140 | end 141 | 142 | defp extract_url(_meta, conn) do 143 | build_url(conn.scheme, conn.host, conn.port, conn.request_path) 144 | end 145 | 146 | defp extract_request_path(%{error: _} = meta, _conn) do 147 | case Map.get(meta, :request_target) do 148 | nil -> "" 149 | {_, _, _, path} -> path || "" 150 | end 151 | end 152 | 153 | defp extract_request_path(_meta, conn) do 154 | conn.request_path 155 | end 156 | 157 | defp build_url(scheme, host, port, path), do: "#{scheme}://#{host}:#{port}#{path}" 158 | 159 | defp user_agent(conn) do 160 | case Plug.Conn.get_req_header(conn, "user-agent") do 161 | [] -> "" 162 | [head | _] -> head 163 | end 164 | end 165 | 166 | defp client_ip(%{remote_ip: remote_ip} = conn) do 167 | case Plug.Conn.get_req_header(conn, "x-forwarded-for") do 168 | [] -> 169 | remote_ip 170 | |> :inet_parse.ntoa() 171 | |> to_string() 172 | 173 | [ip_address | _] -> 174 | ip_address 175 | end 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /test/bandit/opentelemetry_bandit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule OpentelemetryBanditTest do 2 | use ExUnit.Case, async: true 3 | 4 | require OpenTelemetry.Tracer 5 | require OpenTelemetry.Span 6 | require Record 7 | 8 | use ServerHelper 9 | 10 | for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/otel_span.hrl") do 11 | Record.defrecord(name, spec) 12 | end 13 | 14 | describe "http integration" do 15 | test "default span generation for 200" do 16 | Req.get("http://localhost:4000/hello") 17 | 18 | assert_receive {:span, 19 | span( 20 | name: "HTTP GET /hello", 21 | kind: :server, 22 | status: {:status, :ok, _}, 23 | attributes: attributes 24 | )} 25 | 26 | assert %{ 27 | "net.peer.name": "localhost", 28 | "http.method": "GET", 29 | "http.target": "/hello", 30 | "http.scheme": :http, 31 | "http.status_code": 200 32 | } = :otel_attributes.map(attributes) 33 | end 34 | 35 | test "default span generation for 200 without user-agent" do 36 | {:ok, {{_, 200, _}, _, _}} = 37 | :httpc.request(:get, {~c"http://localhost:4000/hello", []}, [], []) 38 | 39 | assert_receive {:span, 40 | span( 41 | name: "HTTP GET /hello", 42 | kind: :server, 43 | status: {:status, :ok, _}, 44 | attributes: attributes 45 | )} 46 | 47 | assert %{ 48 | "net.peer.name": "localhost", 49 | "http.method": "GET", 50 | "http.target": "/hello", 51 | "http.scheme": :http, 52 | "http.status_code": 200, 53 | "http.client_ip": "127.0.0.1" 54 | } = :otel_attributes.map(attributes) 55 | end 56 | 57 | test "default span generation for 200 with x-forwarded-for" do 58 | Req.get("http://localhost:4000/hello", headers: %{x_forwarded_for: "127.0.0.1"}) 59 | 60 | assert_receive {:span, 61 | span( 62 | name: "HTTP GET /hello", 63 | kind: :server, 64 | status: {:status, :ok, _}, 65 | attributes: attributes 66 | )} 67 | 68 | assert %{ 69 | "net.peer.name": "localhost", 70 | "http.method": "GET", 71 | "http.target": "/hello", 72 | "http.scheme": :http, 73 | "http.status_code": 200, 74 | "http.client_ip": "127.0.0.1" 75 | } = :otel_attributes.map(attributes) 76 | end 77 | 78 | test "default span generation for halted connection" do 79 | Req.get("http://localhost:4000/fail", retry: false) 80 | 81 | assert_receive {:span, 82 | span( 83 | name: "HTTP GET /fail", 84 | kind: :server, 85 | status: {:status, :ok, _}, 86 | attributes: attributes 87 | )} 88 | 89 | assert %{ 90 | "net.peer.name": "localhost", 91 | "http.method": "GET", 92 | "http.target": "/fail", 93 | "http.scheme": :http, 94 | "http.status_code": 500 95 | } = :otel_attributes.map(attributes) 96 | end 97 | 98 | test "default span generation for 500 response" do 99 | :telemetry.execute( 100 | [:bandit, :request, :stop], 101 | %{duration: 444, resp_body_bytes: 10}, 102 | %{ 103 | conn: nil, 104 | status: 500, 105 | error: "Internal Server Error", 106 | method: "GET", 107 | request_target: {nil, nil, nil, "/not_existing_route"} 108 | } 109 | ) 110 | 111 | assert_receive {:span, 112 | span( 113 | name: "HTTP GET /not_existing_route", 114 | kind: :error, 115 | status: {:status, :error, "Internal Server Error"}, 116 | attributes: attributes 117 | )} 118 | 119 | assert %{ 120 | "http.url": _, 121 | "http.method": "GET", 122 | "http.status_code": 500, 123 | "http.response_content_length": 10, 124 | "net.transport": :"IP.TCP" 125 | } = :otel_attributes.map(attributes) 126 | end 127 | 128 | test "span when request_target is empty" do 129 | :telemetry.execute( 130 | [:bandit, :request, :stop], 131 | %{duration: 444, resp_body_bytes: 10}, 132 | %{ 133 | conn: nil, 134 | status: 500, 135 | error: "Internal Server Error", 136 | method: "GET", 137 | request_target: nil 138 | } 139 | ) 140 | 141 | assert_receive {:span, 142 | span( 143 | name: "HTTP GET", 144 | kind: :error, 145 | status: {:status, :error, "Internal Server Error"}, 146 | attributes: attributes 147 | )} 148 | 149 | assert %{ 150 | "http.url": _, 151 | "http.method": "GET", 152 | "http.status_code": 500, 153 | "http.response_content_length": 10, 154 | "net.transport": :"IP.TCP" 155 | } = :otel_attributes.map(attributes) 156 | end 157 | 158 | test "exception catch span" do 159 | Req.get("http://localhost:4000/exception", retry: false) 160 | 161 | assert_receive {:span, 162 | span( 163 | name: "HTTP exception RuntimeError", 164 | kind: :error, 165 | status: {:status, :error, _} 166 | )} 167 | end 168 | end 169 | 170 | describe "websocket integration" do 171 | test "span when request finished successfully" do 172 | :telemetry.execute( 173 | [:bandit, :websocket, :stop], 174 | %{ 175 | duration: 444, 176 | send_binary_frame_bytes: 10, 177 | recv_binary_frame_bytes: 15 178 | }, 179 | %{} 180 | ) 181 | 182 | assert_receive {:span, 183 | span( 184 | name: "Websocket", 185 | kind: :server, 186 | status: {:status, :ok, _}, 187 | attributes: attributes 188 | )} 189 | 190 | assert %{ 191 | "net.transport": :websocket, 192 | "websocket.recv.binary.frame.bytes": 10, 193 | "websocket.send.binary.frame.bytes": 15 194 | } = :otel_attributes.map(attributes) 195 | end 196 | 197 | test "span when error is set" do 198 | :telemetry.execute( 199 | [:bandit, :websocket, :stop], 200 | %{ 201 | duration: 444, 202 | send_binary_frame_bytes: 10, 203 | recv_binary_frame_bytes: 15 204 | }, 205 | %{error: "Internal Server Error"} 206 | ) 207 | 208 | assert_receive {:span, 209 | span( 210 | name: "Websocket", 211 | kind: :error, 212 | status: {:status, :error, _}, 213 | attributes: attributes 214 | )} 215 | 216 | assert %{ 217 | "net.transport": :websocket, 218 | "websocket.recv.binary.frame.bytes": 10, 219 | "websocket.send.binary.frame.bytes": 15 220 | } = :otel_attributes.map(attributes) 221 | end 222 | end 223 | 224 | setup do 225 | :otel_simple_processor.set_exporter(:otel_exporter_pid, self()) 226 | 227 | {:ok, _} = start_supervised({Bandit, plug: __MODULE__, port: 4000, startup_log: false}) 228 | 229 | OpentelemetryBandit.setup() 230 | 231 | :ok 232 | end 233 | 234 | def hello(conn) do 235 | conn |> send_resp(200, "OK") 236 | end 237 | 238 | def fail(conn) do 239 | conn |> send_resp(500, "Internal Server Error") |> halt() 240 | end 241 | 242 | def exception(_conn) do 243 | raise "boom" 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "acceptor_pool": {:hex, :acceptor_pool, "1.0.0", "43c20d2acae35f0c2bcd64f9d2bde267e459f0f3fd23dab26485bf518c281b21", [:rebar3], [], "hexpm", "0cbcd83fdc8b9ad2eee2067ef8b91a14858a5883cb7cd800e6fcd5803e158788"}, 3 | "bandit": {:hex, :bandit, "1.0.0", "2bd87bbf713d0eed0090f2fa162cd1676198122e6c2b68a201c706e354a6d5e5", [:mix], [{:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "32acf6ac030fee1f99fd9c3fcf81671911ae8637e0a61c98111861b466efafdb"}, 4 | "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, 5 | "chatterbox": {:hex, :ts_chatterbox, "0.13.0", "6f059d97bcaa758b8ea6fffe2b3b81362bd06b639d3ea2bb088335511d691ebf", [:rebar3], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "b93d19104d86af0b3f2566c4cba2a57d2e06d103728246ba1ac6c3c0ff010aa7"}, 6 | "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, 7 | "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 9 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 10 | "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, 11 | "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, 12 | "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, 13 | "gproc": {:hex, :gproc, "0.8.0", "cea02c578589c61e5341fce149ea36ccef236cc2ecac8691fba408e7ea77ec2f", [:rebar3], [], "hexpm", "580adafa56463b75263ef5a5df4c86af321f68694e7786cb057fd805d1e2a7de"}, 14 | "grpcbox": {:hex, :grpcbox, "0.16.0", "b83f37c62d6eeca347b77f9b1ec7e9f62231690cdfeb3a31be07cd4002ba9c82", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.13.0", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.8.0", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "294df743ae20a7e030889f00644001370a4f7ce0121f3bbdaf13cf3169c62913"}, 15 | "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm", "06f580167c4b8b8a6429040df36cc93bba6d571faeaec1b28816523379cbb23a"}, 16 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, 17 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 18 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 19 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 20 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, 21 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 22 | "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, 23 | "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, 24 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 25 | "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, 26 | "opentelemetry": {:hex, :opentelemetry, "1.3.1", "f0a342a74379e3540a634e7047967733da4bc8b873ec9026e224b2bd7369b1fc", [:rebar3], [{:opentelemetry_api, "~> 1.2.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "de476b2ac4faad3e3fe3d6e18b35dec9cb338c3b9910c2ce9317836dacad3483"}, 27 | "opentelemetry_api": {:hex, :opentelemetry_api, "1.2.2", "693f47b0d8c76da2095fe858204cfd6350c27fe85d00e4b763deecc9588cf27a", [:mix, :rebar3], [{:opentelemetry_semantic_conventions, "~> 0.2", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}], "hexpm", "dc77b9a00f137a858e60a852f14007bb66eda1ffbeb6c05d5fe6c9e678b05e9d"}, 28 | "opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.6.0", "f4fbf69aa9f1541b253813221b82b48a9863bc1570d8ecc517bc510c0d1d3d8c", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.3", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.2", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "1802d1dca297e46f21e5832ecf843c451121e875f73f04db87355a6cb2ba1710"}, 29 | "opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "0.2.0", "b67fe459c2938fcab341cb0951c44860c62347c005ace1b50f8402576f241435", [:mix, :rebar3], [], "hexpm", "d61fa1f5639ee8668d74b527e6806e0503efc55a42db7b5f39939d84c07d6895"}, 30 | "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.0.0", "d5982a319e725fcd2305b306b65c18a86afdcf7d96821473cf0649ff88877615", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.0", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "3401d13a1d4b7aa941a77e6b3ec074f0ae77f83b5b2206766ce630123a9291a9"}, 31 | "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, 32 | "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, 33 | "req": {:hex, :req, "0.4.4", "a17b6bec956c9af4f08b5d8e8a6fc6e4edf24ccc0ac7bf363a90bba7a0f0138c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2618c0493444fee927d12073afb42e9154e766b3f4448e1011f0d3d551d1a011"}, 34 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 35 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 36 | "telemetry_registry": {:hex, :telemetry_registry, "0.3.1", "14a3319a7d9027bdbff7ebcacf1a438f5f5c903057b93aee484cca26f05bdcba", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6d0ca77b691cf854ed074b459a93b87f4c7f5512f8f7743c635ca83da81f939e"}, 37 | "thousand_island": {:hex, :thousand_island, "1.0.0", "63fc8807d8607c9d74fa670996897c8c8a1f2022c8c68d024182e45249acd756", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "996320c72ba8f34d7be9b02900622e44341649f24359e0f67643e4dda8f23995"}, 38 | "tls_certificate_check": {:hex, :tls_certificate_check, "1.20.0", "1ac0c53f95e201feb8d398ef9d764ae74175231289d89f166ba88a7f50cd8e73", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "ab57b74b1a63dc5775650699a3ec032ec0065005eff1f020818742b7312a8426"}, 39 | "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, 40 | } 41 | --------------------------------------------------------------------------------