├── .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 | 
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 |
--------------------------------------------------------------------------------