├── .chglog ├── CHANGELOG.tpl.md └── config.yml ├── .formatter.exs ├── .github ├── FUNDING.yml └── workflows │ ├── elixir.yml │ └── lints.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── example.png ├── lib └── plug_telemetry_server_timing.ex ├── mix.exs ├── mix.lock └── test ├── plug_telemetry_server_timing_test.exs └── test_helper.exs /.chglog/CHANGELOG.tpl.md: -------------------------------------------------------------------------------- 1 | {{ if .Versions -}} 2 | 3 | ## [Unreleased] 4 | 5 | {{ if .Unreleased.CommitGroups -}} 6 | {{ range .Unreleased.CommitGroups -}} 7 | ### {{ .Title }} 8 | {{ range .Commits -}} 9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} 10 | {{ end }} 11 | {{ end -}} 12 | {{ end -}} 13 | {{ end -}} 14 | 15 | {{ range .Versions }} 16 | 17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }} 18 | {{ range .CommitGroups -}} 19 | ### {{ .Title }} 20 | {{ range .Commits -}} 21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} ([`{{ .Hash.Short }}`]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Long }})) 22 | {{ end }} 23 | {{ end -}} 24 | 25 | {{- if .NoteGroups -}} 26 | {{ range .NoteGroups -}} 27 | ### {{ .Title }} 28 | {{ range .Notes }} 29 | {{ .Body }} 30 | {{ end }} 31 | {{ end -}} 32 | {{ end -}} 33 | {{ end -}} 34 | 35 | {{- if .Versions }} 36 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD 37 | {{ range .Versions -}} 38 | {{ if .Tag.Previous -}} 39 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }} 40 | {{ end -}} 41 | {{ end -}} 42 | {{ end -}} 43 | -------------------------------------------------------------------------------- /.chglog/config.yml: -------------------------------------------------------------------------------- 1 | style: github 2 | template: CHANGELOG.tpl.md 3 | info: 4 | title: CHANGELOG 5 | repository_url: https://github.com/hauleth/mix_unused 6 | options: 7 | commits: 8 | filters: 9 | Type: 10 | - ft 11 | - feat 12 | - fix 13 | - docs 14 | commit_groups: 15 | title_maps: 16 | ft: Features 17 | feat: Features 18 | fix: Bug Fixes 19 | docs: Documentation 20 | header: 21 | pattern: "^(\\w*)\\:\\s(.*)$" 22 | pattern_maps: 23 | - Type 24 | - Subject 25 | notes: 26 | keywords: 27 | - BREAKING CHANGE 28 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [hauleth] 4 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | name: Build and test 13 | # Match Ubuntu release to Erlang release as per 14 | # https://github.com/erlef/setup-beam#compatibility-between-operating-system-and-erlangotp 15 | runs-on: ubuntu-20.04 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Elixir 20 | uses: erlef/setup-beam@v1 21 | with: 22 | elixir-version: '1.10.3' 23 | otp-version: '22.3' 24 | - name: Restore dependencies cache 25 | uses: actions/cache@v2 26 | with: 27 | path: deps 28 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 29 | restore-keys: ${{ runner.os }}-mix- 30 | - name: Install dependencies 31 | run: mix deps.get 32 | - name: Run tests 33 | run: mix test 34 | -------------------------------------------------------------------------------- /.github/workflows/lints.yml: -------------------------------------------------------------------------------- 1 | name: Lints CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | credo: 10 | # Match Ubuntu release to Erlang release as per 11 | # https://github.com/erlef/setup-beam#compatibility-between-operating-system-and-erlangotp 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Elixir 16 | uses: erlef/setup-beam@v1 17 | with: 18 | elixir-version: '1.10.3' 19 | otp-version: '22.3' 20 | - name: Restore dependencies cache 21 | uses: actions/cache@v2 22 | with: 23 | path: deps 24 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 25 | restore-keys: ${{ runner.os }}-mix- 26 | - name: Install dependencies 27 | run: mix deps.get 28 | - name: Run tests 29 | run: mix credo 30 | 31 | format: 32 | # Likewise 33 | runs-on: ubuntu-20.04 34 | steps: 35 | - uses: actions/checkout@v2 36 | - name: Set up Elixir 37 | uses: erlef/setup-beam@v1 38 | with: 39 | elixir-version: '1.10.3' 40 | otp-version: '22.3' 41 | - name: Restore dependencies cache 42 | uses: actions/cache@v2 43 | with: 44 | path: deps 45 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 46 | restore-keys: ${{ runner.os }}-mix- 47 | - name: Install dependencies 48 | run: mix deps.get 49 | - name: Run tests 50 | run: mix format --check-formatted 51 | -------------------------------------------------------------------------------- /.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 | plug_server_timing-*.tar 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [Unreleased] 3 | 4 | 5 | 6 | ## [0.3.0] - 2021-10-04 7 | ### Features 8 | - add support for events configuration ([`272aa39`](https://github.com/hauleth/mix_unused/commit/272aa39d42c4e2aebd52daab4100c2e6aac7b431)) 9 | 10 | 11 | 12 | ## [0.2.2] - 2021-10-04 13 | ### Bug Fixes 14 | - version fetching from Hex v2 ([`9f65167`](https://github.com/hauleth/mix_unused/commit/9f65167e4ca8d1146f778e02453e93ab3f7ba88d)) 15 | 16 | 17 | 18 | ## [0.2.1] - 2021-10-04 19 | ### Bug Fixes 20 | - version fetching from Hex ([`45a1924`](https://github.com/hauleth/mix_unused/commit/45a192423327c466a92154c8b2b5248c421cbbd5)) 21 | 22 | 23 | 24 | ## [0.2.0] - 2021-09-23 25 | ### Bug Fixes 26 | - fetching version from Git ([`b56ae3e`](https://github.com/hauleth/mix_unused/commit/b56ae3e1803e54240e205f2098e783a81b31e025)) 27 | - use better versioning functions in Mixfile ([`fed82c2`](https://github.com/hauleth/mix_unused/commit/fed82c2bd83a597350c8535a3b0998e622687865)) 28 | 29 | ### Features 30 | - allow setting additional options for events ([`80304aa`](https://github.com/hauleth/mix_unused/commit/80304aac3102f10d4428cf6f95d7a6d9815f2dd8)) 31 | 32 | 33 | 34 | ## 0.1.0 - 2019-10-08 35 | 36 | [Unreleased]: https://github.com/hauleth/mix_unused/compare/0.3.0...HEAD 37 | [0.3.0]: https://github.com/hauleth/mix_unused/compare/0.2.2...0.3.0 38 | [0.2.2]: https://github.com/hauleth/mix_unused/compare/0.2.1...0.2.2 39 | [0.2.1]: https://github.com/hauleth/mix_unused/compare/0.2.0...0.2.1 40 | [0.2.0]: https://github.com/hauleth/mix_unused/compare/0.1.0...0.2.0 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Łukasz Niemier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plug.Telemetry.ServerTiming 2 | 3 | 4 | 5 | This library provides support for [`Server-Timing`][st] header in Plug 6 | applications by exposing [Telemetry][tm] events as metrics in HTTP headers. This 7 | allows developers to use their's browser DevTools to display server metrics in 8 | readable way. 9 | 10 | ## Installation 11 | 12 | The package can be installed by adding `plug_server_timing` to your list of 13 | dependencies in `mix.exs`: 14 | 15 | ```elixir 16 | def deps do 17 | [ 18 | {:plug_telemetry_server_timing, "~> 0.3.0"} 19 | ] 20 | end 21 | ``` 22 | 23 | Then add `Plug.ServerTiming` to your pipeline **BEFORE** any `Plug.Telemetry` 24 | definitions: 25 | 26 | ```elixir 27 | plug Plug.Telemetry.ServerTiming 28 | plug Plug.Telemetry, event_prefix: [:my, :plug] 29 | ``` 30 | 31 | And then you need to `install/1` metrics you will want to see in the DevTools: 32 | 33 | ```elixir 34 | Plug.Telemetry.ServerTiming.install([ 35 | {[:my, :plug, :stop], :duration} 36 | ]) 37 | ``` 38 | 39 | Now when you will open given page in [browsers with support for 40 | `Server-Timing`][caniuse] you will be able to see the data in DevTools, example 41 | in Google Chrome: 42 | 43 | ![Google Chrome DevTools image example](assets/example.png) 44 | 45 | ### Important 46 | 47 | You need to place this plug **BEFORE** `Plug.Telemetry` call as otherwise it 48 | will not see it's events (`before_send` callbacks are called in reverse order 49 | of declaration, so this one need to be added before `Plug.Telemetry` one. 50 | 51 | ## Caveats 52 | 53 | This will not respond with events that happened in separate processes, only 54 | events that happened in the Plug process will be recorded. 55 | 56 | ### WARNING 57 | 58 | Current specification of `Server-Timing` do not provide a way to specify event 59 | start time, which mean, that the data displayed in the DevTools isn't trace 60 | report (like the content of the "regular" HTTP timings) but raw dump of the data 61 | displayed as a bars. This can be a little bit confusing, but right now there is 62 | nothing I can do about it. 63 | 64 | [caniuse]: https://caniuse.com/#feat=server-timing 65 | [st]: https://w3c.github.io/server-timing/#the-server-timing-header-field 66 | [tm]: https://github.com/beam-telemetry/telemetry 67 | 68 | 69 | 70 | ## License 71 | 72 | [MIT License](LICENSE) 73 | -------------------------------------------------------------------------------- /assets/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hauleth/plug_telemetry_server_timing/36cbe16d1f43363b04cd18a152ada22d6314b5cb/assets/example.png -------------------------------------------------------------------------------- /lib/plug_telemetry_server_timing.ex: -------------------------------------------------------------------------------- 1 | defmodule Plug.Telemetry.ServerTiming do 2 | @behaviour Plug 3 | 4 | @external_resource "README.md" 5 | @moduledoc File.read!("README.md") 6 | |> String.split(~r//, parts: 3) 7 | |> Enum.at(1) 8 | 9 | import Plug.Conn 10 | 11 | @impl true 12 | @doc false 13 | def init(opts), do: opts 14 | 15 | @impl true 16 | @doc false 17 | def call(conn, _opts) do 18 | enabled = Application.fetch_env!(:plug_telemetry_server_timing, :enabled) 19 | 20 | if enabled do 21 | start = System.monotonic_time() 22 | Process.put(__MODULE__, {enabled, []}) 23 | register_before_send(conn, &timings(&1, start)) 24 | else 25 | conn 26 | end 27 | end 28 | 29 | @type events() :: [event()] 30 | @type event() :: 31 | {:telemetry.event_name(), measurement :: atom()} 32 | | {:telemetry.event_name(), measurement :: atom(), opts :: keyword() | map()} 33 | 34 | @doc """ 35 | Define which events should be available within response headers. 36 | 37 | Tuple values are: 38 | 39 | 1. List of atoms that is the name of the event that we should listen for. 40 | 2. Atom that contains the name of the metric that should be recorded. 41 | 3. Optionally keyword list or map with additional options. Currently 42 | supported options are: 43 | 44 | - `:name` - alternative name for the metric. By default it will be 45 | constructed by joining event name and name of metric with dots. 46 | Ex. for `{[:foo, :bar], :baz}` default metric name will be `foo.bar.baz`. 47 | - `:description` - string that will be set as `desc`. 48 | 49 | ## Example 50 | 51 | ```elixir 52 | #{inspect(__MODULE__)}.install([ 53 | {[:phoenix, :endpoint, :stop], :duration, description: "Phoenix time"}, 54 | {[:my_app, :repo, :query], :total_time, description: "DB request"} 55 | ]) 56 | ``` 57 | """ 58 | @spec install(events()) :: :ok 59 | def install(events) do 60 | for event <- events, 61 | {metric_name, metric, opts} = normalise(event) do 62 | name = Map.get_lazy(opts, :name, fn -> "#{Enum.join(metric_name, ".")}.#{metric}" end) 63 | description = Map.get(opts, :description, "") 64 | 65 | :ok = 66 | :telemetry.attach( 67 | {__MODULE__, name}, 68 | metric_name, 69 | &__MODULE__.__handle__/4, 70 | {metric, %{name: name, desc: description}} 71 | ) 72 | end 73 | 74 | :ok 75 | end 76 | 77 | defp normalise({name, metric}), do: {name, metric, %{}} 78 | defp normalise({name, metric, opts}) when is_map(opts), do: {name, metric, opts} 79 | defp normalise({name, metric, opts}) when is_list(opts), do: {name, metric, Map.new(opts)} 80 | 81 | @doc false 82 | def __handle__(metric_name, measurements, metadata, {metric, opts}) do 83 | with {true, data} <- Process.get(__MODULE__), 84 | %{^metric => duration} <- measurements do 85 | current = System.monotonic_time() 86 | 87 | Process.put( 88 | __MODULE__, 89 | {true, 90 | [{duration, current, update_desc(opts, {metric_name, measurements, metadata})} | data]} 91 | ) 92 | end 93 | 94 | :ok 95 | end 96 | 97 | defp update_desc(%{desc: cb} = opts, {name, meas, meta}) when is_function(cb, 3) do 98 | %{opts | desc: cb.(name, meas, meta)} 99 | end 100 | 101 | defp update_desc(%{desc: desc} = opts, _) when is_binary(desc) do 102 | opts 103 | end 104 | 105 | defp timings(conn, start) do 106 | case Process.get(__MODULE__) do 107 | {true, measurements} -> 108 | value = 109 | measurements 110 | |> Enum.reverse() 111 | |> Enum.map_join(",", &encode(&1, start)) 112 | 113 | put_resp_header(conn, "server-timing", value) 114 | 115 | _ -> 116 | conn 117 | end 118 | end 119 | 120 | defp encode({measurement, timestamp, opts}, start) do 121 | %{desc: desc, name: name} = opts 122 | scale = System.convert_time_unit(1, :millisecond, :native) 123 | 124 | [ 125 | name, 126 | {"dur", measurement / scale}, 127 | {"total", (timestamp - start) / scale}, 128 | {"desc", desc} 129 | ] 130 | |> Enum.flat_map(&build/1) 131 | |> Enum.join(";") 132 | end 133 | 134 | defp build({_name, empty}) when empty in [nil, ""], do: [] 135 | defp build({name, value}) when is_number(value), do: [[name, ?=, to_string(value)]] 136 | defp build({name, value}), do: [[name, ?=, Jason.encode!(to_string(value))]] 137 | defp build(name), do: [name] 138 | end 139 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule PlugServerTiming.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | ver = version() 6 | 7 | [ 8 | app: :plug_telemetry_server_timing, 9 | version: ver, 10 | description: "Plug for providing Telemetry metrics within browser DevTools", 11 | elixir: "~> 1.10", 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | source_url: "https://github.com/hauleth/plug_telemetry_server_timing", 15 | docs: [ 16 | assets: "assets/", 17 | main: "Plug.Telemetry.ServerTiming" 18 | ], 19 | package: [ 20 | source_ref: ver, 21 | licenses: ["MIT"], 22 | links: %{ 23 | "GitHub" => "https://github.com/hauleth/plug_telemetry_server_timing" 24 | } 25 | ] 26 | ] 27 | end 28 | 29 | # Run "mix help compile.app" to learn about applications. 30 | def application do 31 | [ 32 | env: [ 33 | enabled: true 34 | ] 35 | ] 36 | end 37 | 38 | # Run "mix help deps" to learn about dependencies. 39 | defp deps do 40 | [ 41 | {:telemetry, "~> 0.4.0 or ~> 1.0"}, 42 | {:jason, "~> 1.0"}, 43 | {:plug, "~> 1.0"}, 44 | {:ex_doc, ">= 0.0.0", only: [:dev]}, 45 | {:credo, ">= 0.0.0", only: [:dev]} 46 | ] 47 | end 48 | 49 | defp version do 50 | with :error <- hex_version(), 51 | :error <- git_version() do 52 | "0.0.0-dev" 53 | else 54 | {:ok, ver} -> ver 55 | end 56 | end 57 | 58 | defp hex_version do 59 | with {:ok, terms} <- :file.consult("hex_metadata.config"), 60 | {"version", version} <- List.keyfind(terms, "version", 0) do 61 | {:ok, version} 62 | else 63 | _ -> :error 64 | end 65 | end 66 | 67 | defp git_version do 68 | System.cmd("git", ~w[describe]) 69 | else 70 | {ver, 0} -> {:ok, String.trim(ver)} 71 | _ -> :error 72 | catch 73 | _, _ -> :error 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, 3 | "credo": {:hex, :credo, "1.5.6", "e04cc0fdc236fefbb578e0c04bd01a471081616e741d386909e527ac146016c6", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4b52a3e558bd64e30de62a648518a5ea2b6e3e5d2b164ef5296244753fc7eb17"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.15", "b29e8e729f4aa4a00436580dcc2c9c5c51890613457c193cc8525c388ccb2f06", [:mix], [], "hexpm", "044523d6438ea19c1b8ec877ec221b008661d3c27e3b848f4c879f500421ca5c"}, 5 | "ex_doc": {:hex, :ex_doc, "0.25.3", "3edf6a0d70a39d2eafde030b8895501b1c93692effcbd21347296c18e47618ce", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "9ebebc2169ec732a38e9e779fd0418c9189b3ca93f4a676c961be6c1527913f5"}, 6 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 7 | "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, 8 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 9 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.1", "b5888c880d17d1cc3e598f05cdb5b5a91b7b17ac4eaf5f297cb697663a1094dd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "db68c173234b07ab2a07f645a5acdc117b9f99d69ebf521821d89690ae6c6ec8"}, 10 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 11 | "mime": {:hex, :mime, "2.0.1", "0de4c81303fe07806ebc2494d5321ce8fb4df106e34dd5f9d787b637ebadc256", [:mix], [], "hexpm", "7a86b920d2aedce5fb6280ac8261ac1a739ae6c1a1ad38f5eadf910063008942"}, 12 | "nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"}, 13 | "plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"}, 14 | "plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"}, 15 | "telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"}, 16 | } 17 | -------------------------------------------------------------------------------- /test/plug_telemetry_server_timing_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Plug.ServerTimingTest do 2 | use ExUnit.Case, async: false 3 | use Plug.Test 4 | 5 | @subject Plug.Telemetry.ServerTiming 6 | 7 | doctest @subject 8 | 9 | ExUnit.Case.register_attribute(__MODULE__, :events, accumulate: true) 10 | 11 | setup ctx do 12 | events = ctx.registered.events 13 | 14 | @subject.install(events) 15 | 16 | on_exit(fn -> 17 | for %{id: {@subject, _} = id} <- :telemetry.list_handlers([]) do 18 | :telemetry.detach(id) 19 | end 20 | end) 21 | end 22 | 23 | test "if no events defined then there is no header" do 24 | conn = request() 25 | 26 | assert [] == get_resp_header(conn, "server-timing") 27 | end 28 | 29 | @events {[:prefix, :stop], :duration} 30 | test "if defined listener in event then it is present in header" do 31 | conn = request([{Plug.Telemetry, event_prefix: [:prefix]}]) 32 | 33 | assert [measure] = get_timings(conn) 34 | assert {"prefix.stop.duration", %{"dur" => _}} = measure 35 | end 36 | 37 | @events {[:foo], :bar} 38 | test "custom Telemetry events also can be recorded" do 39 | conn = request() 40 | 41 | dur = System.convert_time_unit(2, :millisecond, :native) 42 | :telemetry.execute([:foo], %{bar: dur}) 43 | 44 | assert [measure] = get_timings(conn) 45 | assert {"foo.bar", %{"dur" => "2.0"}} = measure 46 | end 47 | 48 | @events {[:foo], :bar} 49 | @events {[:bar], :baz} 50 | test "two different events are recorded" do 51 | conn = request() 52 | 53 | dur = System.convert_time_unit(2, :millisecond, :native) 54 | :telemetry.execute([:foo], %{bar: dur}) 55 | :telemetry.execute([:bar], %{baz: 0}) 56 | 57 | timings = get_timings(conn) 58 | assert {"foo.bar", %{"dur" => "2.0"}} = List.keyfind(timings, "foo.bar", 0) 59 | assert {"bar.baz", %{"dur" => "0.0"}} = List.keyfind(timings, "bar.baz", 0) 60 | end 61 | 62 | @events {[:foo], :bar} 63 | @events {[:foo], :baz} 64 | test "two measurements for same event are recorded" do 65 | conn = request() 66 | 67 | dur = System.convert_time_unit(2500, :microsecond, :native) 68 | :telemetry.execute([:foo], %{bar: dur, baz: 0}) 69 | 70 | timings = get_timings(conn) 71 | assert {"foo.bar", %{"dur" => "2.5"}} = List.keyfind(timings, "foo.bar", 0) 72 | assert {"foo.baz", %{"dur" => "0.0"}} = List.keyfind(timings, "foo.baz", 0) 73 | end 74 | 75 | @events {[:foo], :bar, description: "Hi"} 76 | test "we can add description to the measurement" do 77 | conn = request() 78 | 79 | :telemetry.execute([:foo], %{bar: 0}) 80 | 81 | timings = get_timings(conn) 82 | assert {"foo.bar", %{"desc" => ~S("Hi")}} = List.keyfind(timings, "foo.bar", 0) 83 | end 84 | 85 | @events {[:foo], :bar, name: "qux"} 86 | test "we can change name of the produced value" do 87 | conn = request() 88 | 89 | :telemetry.execute([:foo], %{bar: 0}) 90 | 91 | timings = get_timings(conn) 92 | assert {"qux", _} = List.keyfind(timings, "qux", 0) 93 | refute List.keyfind(timings, "foo.bar", 0) 94 | end 95 | 96 | @events {[:prefix, :stop], :duration} 97 | test "events that aren't listened are ignored" do 98 | conn = request([{Plug.Telemetry, event_prefix: [:prefix]}]) 99 | 100 | dur = System.convert_time_unit(2, :millisecond, :native) 101 | :telemetry.execute([:foo], %{bar: dur}) 102 | 103 | assert [measure] = get_timings(conn) 104 | assert {"prefix.stop.duration", %{"dur" => _}} = measure 105 | end 106 | 107 | @events {[:foo], :bar} 108 | test "when disabled the metrics aren't recorded" do 109 | Application.put_env(:plug_telemetry_server_timing, :enabled, false) 110 | 111 | conn = request() 112 | 113 | dur = System.convert_time_unit(2, :millisecond, :native) 114 | :telemetry.execute([:foo], %{bar: dur}) 115 | 116 | assert [] == get_timings(conn) 117 | after 118 | Application.put_env(:plug_telemetry_server_timing, :enabled, true) 119 | end 120 | 121 | defp request(plugs \\ []) do 122 | opts = @subject.init([]) 123 | 124 | conn = 125 | conn(:get, "/") 126 | |> resp(:ok, "OK") 127 | |> @subject.call(opts) 128 | 129 | Enum.reduce_while(plugs, conn, fn 130 | _plug, %Plug.Conn{halted: true} -> {:halt, conn} 131 | {mod, opts}, conn -> {:cont, mod.call(conn, mod.init(opts))} 132 | end) 133 | end 134 | 135 | # Fetch and parse metrics from the Plug.Conn 136 | defp get_timings(conn) do 137 | entries = 138 | conn 139 | |> try_send_resp() 140 | |> get_resp_header("server-timing") 141 | 142 | Enum.flat_map(entries, &decode/1) 143 | end 144 | 145 | defp try_send_resp(%Plug.Conn{state: :sent} = conn), do: conn 146 | defp try_send_resp(conn), do: send_resp(conn) 147 | 148 | defp decode(row) do 149 | for measure <- String.split(row, ",", trim: true) do 150 | [name | kv] = String.split(measure, ";", trim: true) 151 | 152 | values = 153 | for entry <- kv, into: %{} do 154 | [key, value] = 155 | entry 156 | |> String.trim() 157 | |> String.split("=", limit: 2) 158 | 159 | {key, value} 160 | end 161 | 162 | {name, values} 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------