Compare commits
114 commits
Author | SHA1 | Date | |
---|---|---|---|
3fb227f5d9 | |||
d74c8e3cb3 | |||
fad27f142e | |||
b96516405e | |||
a191077cc4 | |||
de29cb2b51 | |||
d9fe59958e | |||
07c41153f5 | |||
dfe52b6e74 | |||
c5a337ca4a | |||
c440a88bdc | |||
4ad256acdd | |||
de160bd70c | |||
11b8444b96 | |||
60664ff5cc | |||
d8e7e0c3a0 | |||
e43c31997e | |||
f739d4b7e1 | |||
cd489acd63 | |||
09433f8e26 | |||
adc2453081 | |||
cac6f82746 | |||
8cf2027f28 | |||
638badbba8 | |||
a701a200b0 | |||
b11b37078e | |||
7ffec1bed6 | |||
19f4a9b387 | |||
c016c5a8ae | |||
59ae3a59cf | |||
331827d6b4 | |||
c6b7c98930 | |||
08976af06c | |||
69a32accd4 | |||
0dd15ad75e | |||
6bf61d35f1 | |||
ec239d1e57 | |||
bbc6f05d20 | |||
82e4b461eb | |||
384257a33f | |||
d4d38b7b34 | |||
57e0f63ee8 | |||
6eb6fd6a1f | |||
90c60759ca | |||
886e032c05 | |||
3545d01262 | |||
e1e796e234 | |||
d7d16fd925 | |||
0c9137cfe9 | |||
204dd61258 | |||
51d100898c | |||
b6e2337dab | |||
304fd0bcd4 | |||
1008da98a4 | |||
5d8be9ae42 | |||
2be599db1b | |||
e32b2a868d | |||
6009a19705 | |||
055b0ca2d5 | |||
b8b5623a2f | |||
4766d48dcb | |||
db3abee0fb | |||
99aaa5d57d | |||
4ce0d89652 | |||
d78dca56f6 | |||
989d48c46d | |||
27858f1474 | |||
7093ae3517 | |||
6e63fa3961 | |||
fe61ed4e1a | |||
7e5ec8c11c | |||
ac3440d512 | |||
bbf213825b | |||
5a411f54ee | |||
f3c49db9db | |||
c6f84bb1c5 | |||
5c70adaaff | |||
35dce07e77 | |||
4747c50673 | |||
b35d678050 | |||
8791e1228d | |||
5d491362d2 | |||
2e9f41c00d | |||
3ed6b64e80 | |||
2b79e3cb10 | |||
25fb114808 | |||
c672e2c4f4 | |||
7e2f0f2182 | |||
41ac07b8f1 | |||
0d186e834a | |||
306a0d7935 | |||
2736fdbbd1 | |||
e48f26dd85 | |||
b59af873e8 | |||
5cfb9a4a7d | |||
52632fe88e | |||
d95e23d727 | |||
25c9003193 | |||
ccdfce9850 | |||
d57e6a0b7f | |||
5ec7cf267a | |||
48bc11159a | |||
3383898512 | |||
ca1a344057 | |||
3312d02642 | |||
e4224a0daf | |||
49c30c4dde | |||
3ebb9843da | |||
f192b9fd5f | |||
76f2a299f8 | |||
37a064c507 | |||
8358d2de28 | |||
c142bdbc1a | |||
545384b149 |
33 changed files with 2964 additions and 2411 deletions
5
.dialyzer_ignore.exs
Normal file
5
.dialyzer_ignore.exs
Normal file
|
@ -0,0 +1,5 @@
|
|||
[
|
||||
{"lib/wayfarer/server/proxy.ex", :unknown_function},
|
||||
{"lib/wayfarer/target/check.ex", :unknown_function},
|
||||
{"test/support/http_request.ex", :unknown_function}
|
||||
]
|
29
.drone.yml
29
.drone.yml
|
@ -49,7 +49,7 @@ steps:
|
|||
- .rebar3
|
||||
|
||||
- name: install dependencies
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
pull: "always"
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
|
@ -122,7 +122,7 @@ steps:
|
|||
- .rebar3
|
||||
|
||||
- name: mix compile
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -135,7 +135,7 @@ steps:
|
|||
- asdf mix compile --warnings-as-errors
|
||||
|
||||
- name: mix test
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -148,7 +148,7 @@ steps:
|
|||
- asdf mix test
|
||||
|
||||
- name: mix credo
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -161,7 +161,7 @@ steps:
|
|||
- asdf mix credo --strict
|
||||
|
||||
- name: mix hex.audit
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -174,7 +174,7 @@ steps:
|
|||
- asdf mix hex.audit
|
||||
|
||||
- name: mix format
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -187,7 +187,7 @@ steps:
|
|||
- asdf mix format --check-formatted
|
||||
|
||||
- name: mix deps.unlock
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -200,7 +200,7 @@ steps:
|
|||
- asdf mix deps.unlock --check-unused
|
||||
|
||||
- name: mix doctor
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -213,7 +213,7 @@ steps:
|
|||
- asdf mix doctor --full
|
||||
|
||||
- name: mix dialyzer
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -226,7 +226,7 @@ steps:
|
|||
- asdf mix dialyzer
|
||||
|
||||
- name: mix git_ops.check_message
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
environment:
|
||||
MIX_ENV: test
|
||||
HEX_HOME: /drone/src/.hex
|
||||
|
@ -240,7 +240,7 @@ steps:
|
|||
- asdf mix git_ops.check_message .last_commit_message
|
||||
|
||||
- name: mix git_ops.release
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
when:
|
||||
branch:
|
||||
- main
|
||||
|
@ -280,7 +280,7 @@ steps:
|
|||
- fi
|
||||
|
||||
- name: build artifacts
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
@ -327,7 +327,7 @@ steps:
|
|||
settings:
|
||||
api_key:
|
||||
from_secret: DRONE_TOKEN
|
||||
base_url: https://code.harton.nz
|
||||
base_url: https://harton.dev
|
||||
files: artifacts/*.tar.gz
|
||||
checksum: sha256
|
||||
|
||||
|
@ -351,12 +351,11 @@ steps:
|
|||
commands:
|
||||
- mc alias set store $${S3_ENDPOINT} $${ACCESS_KEY} $${SECRET_KEY}
|
||||
- mc mb -p store/docs.harton.nz
|
||||
- mc anonymous set download store/docs.harton.nz
|
||||
- mc mirror --overwrite doc/ store/docs.harton.nz/$${DRONE_REPO}/$${DRONE_TAG}
|
||||
- mc mirror --overwrite doc/ store/docs.harton.nz/$${DRONE_REPO}
|
||||
|
||||
- name: hex release
|
||||
image: code.harton.nz/james/asdf_container:latest
|
||||
image: harton.dev/james/asdf_container:latest
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
|
|
@ -38,7 +38,12 @@ spark_locals_without_parens = [
|
|||
targets: 1,
|
||||
thousand_island_options: 1,
|
||||
threshold: 1,
|
||||
websocket_options: 1
|
||||
transport: 1,
|
||||
websocket_options: 1,
|
||||
ws: 2,
|
||||
ws: 3,
|
||||
wss: 2,
|
||||
wss: 3
|
||||
]
|
||||
|
||||
[
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
erlang 26.1.2
|
||||
elixir 1.15.7
|
||||
erlang 27.0.1
|
||||
elixir 1.17.2
|
||||
hey 0.1.4
|
||||
|
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"cSpell.words": ["ntoa"]
|
||||
}
|
44
CHANGELOG.md
44
CHANGELOG.md
|
@ -5,7 +5,43 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
|
|||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v0.4.0](https://code.harton.nz/james/wayfarer/compare/v0.3.0...v0.4.0) (2023-11-19)
|
||||
## [v0.6.1](https://harton.dev/james/wayfarer/compare/v0.6.0...v0.6.1) (2024-08-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Proxy: don't crash for responses with no body.
|
||||
|
||||
## [v0.6.0](https://harton.dev/james/wayfarer/compare/v0.5.0...v0.6.0) (2024-08-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Add request telemetry (#114)
|
||||
|
||||
## [v0.5.0](https://harton.dev/james/wayfarer/compare/v0.4.0...v0.5.0) (2024-08-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Add support for proxying websockets.
|
||||
|
||||
## [v0.4.1](https://harton.dev/james/wayfarer/compare/v0.4.0...v0.4.1) (2024-06-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* incorrect handling of Mint stream failure.
|
||||
|
||||
## [v0.4.0](https://harton.dev/james/wayfarer/compare/v0.3.0...v0.4.0) (2023-11-19)
|
||||
|
||||
|
||||
|
||||
|
@ -14,7 +50,7 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
|
|||
|
||||
* add proxying. (#7)
|
||||
|
||||
## [v0.3.0](https://code.harton.nz/james/wayfarer/compare/v0.2.0...v0.3.0) (2023-10-14)
|
||||
## [v0.3.0](https://harton.dev/james/wayfarer/compare/v0.2.0...v0.3.0) (2023-10-14)
|
||||
|
||||
|
||||
|
||||
|
@ -27,7 +63,7 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
|
|||
|
||||
* Listener: Register listeners with scheme, address and port.
|
||||
|
||||
## [v0.2.0](https://code.harton.nz/james/wayfarer/compare/v0.1.0...v0.2.0) (2023-10-14)
|
||||
## [v0.2.0](https://harton.dev/james/wayfarer/compare/v0.1.0...v0.2.0) (2023-10-14)
|
||||
|
||||
|
||||
|
||||
|
@ -36,7 +72,7 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
|
|||
|
||||
* Add ability to start and stop HTTP listeners. (#1)
|
||||
|
||||
## [v0.1.0](https://code.harton.nz/james/wayfarer/compare/v0.1.0...v0.1.0) (2023-10-13)
|
||||
## [v0.1.0](https://harton.dev/james/wayfarer/compare/v0.1.0...v0.1.0) (2023-10-13)
|
||||
|
||||
|
||||
|
||||
|
|
21
README.md
21
README.md
|
@ -1,6 +1,6 @@
|
|||
# Wayfarer
|
||||
|
||||
[![Build Status](https://drone.harton.nz/api/badges/james/wayfarer/status.svg?ref=refs/heads/main)](https://drone.harton.nz/james/wayfarer)
|
||||
[![Build Status](https://drone.harton.dev/api/badges/james/wayfarer/status.svg?ref=refs/heads/main)](https://drone.harton.dev/james/wayfarer)
|
||||
[![Hippocratic License HL3-FULL](https://img.shields.io/static/v1?label=Hippocratic%20License&message=HL3-FULL&labelColor=5e2751&color=bc8c3d)](https://firstdonoharm.dev/version/3/0/full.html)
|
||||
|
||||
Wayfarer is a runtime-configurable HTTP reverse proxy using
|
||||
|
@ -9,22 +9,31 @@ Wayfarer is a runtime-configurable HTTP reverse proxy using
|
|||
|
||||
## Status
|
||||
|
||||
Wayfarer is yet to handle it's first HTTP request. Please hold.
|
||||
Wayfarer is able to proxy HTTP/1, HTTP/2 and WebSocket requests. There are
|
||||
probably still edge cases and bugs, but it can be used.
|
||||
|
||||
## Installation
|
||||
|
||||
Wayfarer is not yet available on Hex, so you will need to add it as a Git
|
||||
dependency in your app:
|
||||
Wayfarer is [available in Hex](https://hex.pm/packages/wayfarer), the package
|
||||
can be installed by adding `wayfarer` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:wayfarer, git: "https://code.harton.nz/james/wayfarer.git", tag: "v0.1.0"}
|
||||
{:wayfarer, "~> 0.4.1"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation for `main` is always available on [my docs site](https://docs.harton.nz/james/wayfarer/Wayfarer.html).
|
||||
Documentation for the latest release can be found on
|
||||
[HexDocs](https://hexdocs.pm/wayfarer) and for the `main` branch on
|
||||
[docs.harton.nz](https://docs.harton.nz/james/wayfarer).
|
||||
|
||||
## Github Mirror
|
||||
|
||||
This repository is mirrored [on Github](https://github.com/jimsynz/wayfarer)
|
||||
from it's primary location [on my Forgejo instance](https://harton.dev/james/wayfarer).
|
||||
Feel free to raise issues and open PRs on Github.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import Config
|
|||
config :git_ops,
|
||||
mix_project: Mix.Project.get!(),
|
||||
changelog_file: "CHANGELOG.md",
|
||||
repository_url: "https://code.harton.nz/james/wayfarer",
|
||||
repository_url: "https://harton.dev/james/wayfarer",
|
||||
manage_mix_version?: true,
|
||||
version_tag_prefix: "v",
|
||||
manage_readme_version: "README.md"
|
||||
|
|
File diff suppressed because it is too large
Load diff
1240
documentation/dsls/DSL:-Wayfarer.md
Normal file
1240
documentation/dsls/DSL:-Wayfarer.md
Normal file
File diff suppressed because one or more lines are too long
|
@ -85,7 +85,7 @@ defmodule Wayfarer.Dsl.HealthCheck do
|
|||
doc: "Path"
|
||||
],
|
||||
success_codes: [
|
||||
type: {:wrap_list, {:struct, Range}},
|
||||
type: {:wrap_list, {:or, [{:struct, Range}, {:in, 100..500}]}},
|
||||
required: false,
|
||||
default: @defaults[:success_codes],
|
||||
doc: "HTTP status codes which are considered successful."
|
||||
|
@ -125,22 +125,34 @@ defmodule Wayfarer.Dsl.HealthCheck do
|
|||
@doc false
|
||||
@spec transform(t) :: {:ok, t} | {:error, any}
|
||||
def transform(check) do
|
||||
with :ok <- verify_success_codes(check.success_codes) do
|
||||
maybe_set_name(check)
|
||||
with {:ok, success_codes} <- transform_success_codes(check.success_codes) do
|
||||
maybe_set_name(%{check | success_codes: success_codes})
|
||||
end
|
||||
end
|
||||
|
||||
defguardp is_valid_status_code?(code) when is_integer(code) and code >= 100 and code <= 599
|
||||
|
||||
defguardp is_valid_range?(range)
|
||||
when is_struct(range, Range) and is_integer(range.first) and range.first >= 100 and
|
||||
is_integer(range.last) and range.last <= 500
|
||||
when is_struct(range, Range) and is_valid_status_code?(range.first) and
|
||||
is_valid_status_code?(range.last)
|
||||
|
||||
defp verify_success_codes(range) when is_valid_range?(range), do: :ok
|
||||
defp verify_success_codes([]), do: :ok
|
||||
defp transform_success_codes(range) when is_valid_range?(range), do: {:ok, [range]}
|
||||
defp transform_success_codes([]), do: {:ok, []}
|
||||
|
||||
defp verify_success_codes([head | tail]) when is_valid_range?(head),
|
||||
do: verify_success_codes(tail)
|
||||
defp transform_success_codes([head | tail]) when is_valid_range?(head) do
|
||||
with {:ok, tail} <- transform_success_codes(tail) do
|
||||
{:ok, [head | tail]}
|
||||
end
|
||||
end
|
||||
|
||||
defp verify_success_codes([range | _]),
|
||||
defp transform_success_codes([head | tail])
|
||||
when is_integer(head) and is_valid_status_code?(head) do
|
||||
with {:ok, tail} <- transform_success_codes(tail) do
|
||||
{:ok, [head..head | tail]}
|
||||
end
|
||||
end
|
||||
|
||||
defp transform_success_codes([range | _]),
|
||||
do: {:error, "Value `#{inspect(range)}` is not valid. Must be a range between 100..599"}
|
||||
|
||||
defp maybe_set_name(check) when is_binary(check.name), do: {:ok, check}
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Wayfarer.Dsl.Target do
|
|||
A struct for storing a target generated by the DSL.
|
||||
"""
|
||||
|
||||
alias Spark.{Dsl.Entity, OptionsHelpers}
|
||||
alias Spark.{Dsl.Entity, Options}
|
||||
alias Wayfarer.Dsl.{HealthCheck, HealthChecks}
|
||||
alias Wayfarer.Utils
|
||||
|
||||
|
@ -13,6 +13,7 @@ defmodule Wayfarer.Dsl.Target do
|
|||
name: nil,
|
||||
port: nil,
|
||||
scheme: :http,
|
||||
transport: :auto,
|
||||
uri: nil
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
|
@ -21,7 +22,8 @@ defmodule Wayfarer.Dsl.Target do
|
|||
module: nil | module,
|
||||
name: nil | String.t(),
|
||||
port: :inet.port_number(),
|
||||
scheme: :http | :https | :plug,
|
||||
scheme: :http | :https | :plug | :ws | :wss,
|
||||
transport: :http1 | :http2 | :auto,
|
||||
uri: URI.t()
|
||||
}
|
||||
|
||||
|
@ -40,6 +42,12 @@ defmodule Wayfarer.Dsl.Target do
|
|||
type: :pos_integer,
|
||||
required: true,
|
||||
doc: "The TCP port on which to listen for incoming connections."
|
||||
],
|
||||
transport: [
|
||||
type: {:in, [:http1, :http2, :auto]},
|
||||
required: false,
|
||||
default: :auto,
|
||||
doc: "Which HTTP protocol to use."
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -81,6 +89,28 @@ defmodule Wayfarer.Dsl.Target do
|
|||
],
|
||||
auto_set_fields: [scheme: :plug],
|
||||
args: [:module]
|
||||
},
|
||||
%Entity{
|
||||
name: :ws,
|
||||
target: __MODULE__,
|
||||
schema: @shared_schema,
|
||||
auto_set_fields: [scheme: :ws],
|
||||
args: [:address, :port],
|
||||
imports: [IP.Sigil],
|
||||
transform: {__MODULE__, :transform, []},
|
||||
entities: [health_checks: HealthChecks.entities()],
|
||||
singleton_entity_keys: [:health_checks]
|
||||
},
|
||||
%Entity{
|
||||
name: :wss,
|
||||
target: __MODULE__,
|
||||
schema: @shared_schema,
|
||||
auto_set_fields: [scheme: :wss],
|
||||
args: [:address, :port],
|
||||
imports: [IP.Sigil],
|
||||
transform: {__MODULE__, :transform, []},
|
||||
entities: [health_checks: HealthChecks.entities()],
|
||||
singleton_entity_keys: [:health_checks]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
@ -98,13 +128,13 @@ defmodule Wayfarer.Dsl.Target do
|
|||
@doc false
|
||||
def schema do
|
||||
@shared_schema
|
||||
|> OptionsHelpers.make_optional!(:address)
|
||||
|> OptionsHelpers.make_optional!(:port)
|
||||
|> Options.Helpers.make_optional!(:address)
|
||||
|> Options.Helpers.make_optional!(:port)
|
||||
|> Keyword.merge(
|
||||
scheme: [
|
||||
type: {:in, [:http, :https, :plug]},
|
||||
type: {:in, [:http, :https, :plug, :ws, :wss]},
|
||||
required: true,
|
||||
doc: "The protocol used to talk to the target."
|
||||
doc: "The connection type for the target."
|
||||
],
|
||||
plug: [
|
||||
type: {:behaviour, Plug},
|
||||
|
|
|
@ -98,7 +98,7 @@ defmodule Wayfarer.Dsl.Transformer do
|
|||
{target.scheme, target.module}
|
||||
|
||||
target ->
|
||||
{target.scheme, IP.Address.to_tuple(target.address), target.port}
|
||||
{target.scheme, IP.Address.to_tuple(target.address), target.port, target.transport}
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -113,7 +113,7 @@ defmodule Wayfarer.Dsl.Transformer do
|
|||
|> Map.drop([:address, :health_checks, :port, :uri])
|
||||
|> Enum.to_list()
|
||||
|
||||
target when target.scheme in [:http, :https] ->
|
||||
target when target.scheme in [:http, :https, :ws, :wss] ->
|
||||
health_checks =
|
||||
target.health_checks.health_checks
|
||||
|> Enum.map(&HealthCheck.to_options/1)
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Wayfarer.Listener do
|
|||
|
||||
use GenServer, restart: :transient
|
||||
require Logger
|
||||
alias Spark.OptionsHelpers
|
||||
alias Spark.Options
|
||||
import Wayfarer.Utils
|
||||
|
||||
@options_schema [
|
||||
|
@ -90,7 +90,7 @@ defmodule Wayfarer.Listener do
|
|||
|
||||
## Options
|
||||
|
||||
#{OptionsHelpers.docs(@options_schema)}
|
||||
#{Options.docs(@options_schema)}
|
||||
"""
|
||||
|
||||
@doc false
|
||||
|
@ -126,14 +126,14 @@ defmodule Wayfarer.Listener do
|
|||
{:ok, :https} ->
|
||||
schema =
|
||||
@options_schema
|
||||
|> OptionsHelpers.make_required!(:keyfile)
|
||||
|> OptionsHelpers.make_required!(:certfile)
|
||||
|> OptionsHelpers.make_required!(:cipher_suite)
|
||||
|> Options.Helpers.make_required!(:keyfile)
|
||||
|> Options.Helpers.make_required!(:certfile)
|
||||
|> Options.Helpers.make_required!(:cipher_suite)
|
||||
|
||||
OptionsHelpers.validate(options, schema)
|
||||
Options.validate(options, schema)
|
||||
|
||||
_ ->
|
||||
OptionsHelpers.validate(options, @options_schema)
|
||||
Options.validate(options, @options_schema)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -51,7 +51,17 @@ defmodule Wayfarer.Router do
|
|||
local `Plug`.
|
||||
"""
|
||||
@type target ::
|
||||
{scheme, :inet.ip_address(), :socket.port_number()}
|
||||
{:http | :https | :ws | :wss, :inet.ip_address(), :socket.port_number(),
|
||||
:http1 | :http2 | :auto}
|
||||
| {:plug, module}
|
||||
| {:plug, {module, any}}
|
||||
|
||||
@typedoc """
|
||||
Like `t:target/0` except that it can contain user input for the address portion.
|
||||
"""
|
||||
@type target_input ::
|
||||
{:http | :https | :ws | :wss, Wayfarer.Utils.address_input(), :socket.port_number(),
|
||||
:http1 | :http2 | :auto}
|
||||
| {:plug, module}
|
||||
| {:plug, {module, any}}
|
||||
|
||||
|
@ -85,7 +95,8 @@ defmodule Wayfarer.Router do
|
|||
|
||||
This should only ever be called by `Wayfarer.Server` directly.
|
||||
"""
|
||||
@spec add_route(:ets.tid(), listener, target, [host_name], algorithm) :: :ok | {:error, any}
|
||||
@spec add_route(:ets.tid(), listener, target_input, [host_name], algorithm) ::
|
||||
:ok | {:error, any}
|
||||
def add_route(table, listener, target, host_names, algorithm) do
|
||||
with {:ok, entries} <- route_to_entries(table, listener, target, host_names, algorithm) do
|
||||
:ets.insert(table, entries)
|
||||
|
@ -96,9 +107,9 @@ defmodule Wayfarer.Router do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Add a number of router into the routing table.
|
||||
Add a number of routes into the routing table.
|
||||
"""
|
||||
@spec import_routes(:ets.tid(), [{listener, target, [host_name], algorithm}]) :: :ok
|
||||
@spec import_routes(:ets.tid(), [{listener, target_input, [host_name], algorithm}]) :: :ok
|
||||
def import_routes(table, routes) do
|
||||
with {:ok, entries} <- routes_to_entries(table, routes) do
|
||||
:ets.insert(table, entries)
|
||||
|
@ -142,15 +153,15 @@ defmodule Wayfarer.Router do
|
|||
Change a target's health state.
|
||||
"""
|
||||
@spec update_target_health_status(:ets.tid(), target, health) :: :ok
|
||||
def update_target_health_status(table, {scheme, address, port}, status) do
|
||||
def update_target_health_status(table, {scheme, address, port, transport}, status) do
|
||||
# Match spec generated using:
|
||||
# :ets.fun2ms(fn {listener, host_pattern, {:http, {192, 168, 4, 26}, 80}, algorithm, _} ->
|
||||
# {listener, host_pattern, {:http, {192, 168, 4, 26}, 80}, algorithm, :healthy}
|
||||
# :ets.fun2ms(fn {listener, host_pattern, {:http, {192, 168, 4, 26}, 80, transport}, algorithm, _} ->
|
||||
# {listener, host_pattern, {:http, {192, 168, 4, 26}, 80, transport}, algorithm, :healthy}
|
||||
# end)
|
||||
|
||||
match_spec = [
|
||||
{{:"$1", :"$2", {scheme, address, port}, :"$3", :_}, [],
|
||||
[{{:"$1", :"$2", {{scheme, {address}, port}}, :"$3", status}}]}
|
||||
{{:"$1", :"$2", {scheme, address, port, transport}, :"$3", :_}, [],
|
||||
[{{:"$1", :"$2", {{scheme, {address}, port, transport}}, :"$3", status}}]}
|
||||
]
|
||||
|
||||
:ets.select_replace(table, match_spec)
|
||||
|
@ -223,12 +234,12 @@ defmodule Wayfarer.Router do
|
|||
message: "Value `#{inspect(algorithm)}` is not a valid load balancing algorithm."
|
||||
)}
|
||||
|
||||
defp current_health_state(table, {scheme, address, port}) do
|
||||
defp current_health_state(table, {scheme, address, port, transport}) do
|
||||
# Generated using
|
||||
# :ets.fun2ms(fn {_, _, :target, :_, health} -> health end)
|
||||
|
||||
match_spec = [
|
||||
{{:_, :_, {scheme, address, port}, :_, :"$1"}, [], [:"$1"]}
|
||||
{{:_, :_, {scheme, address, port, transport}, :_, :"$1"}, [], [:"$1"]}
|
||||
]
|
||||
|
||||
case :ets.select(table, match_spec, 1) do
|
||||
|
@ -277,6 +288,14 @@ defmodule Wayfarer.Router do
|
|||
defp sanitise_listener(listener),
|
||||
do: {:error, ArgumentError.exception(message: "Not a valid listener: `#{inspect(listener)}")}
|
||||
|
||||
defp sanitise_transport(transport) when transport in [:http1, :http2, :auto],
|
||||
do: {:ok, transport}
|
||||
|
||||
defp sanitise_transport(transport),
|
||||
do:
|
||||
{:error,
|
||||
ArgumentError.exception(message: "Not a valid target transport: `#{inspect(transport)}`")}
|
||||
|
||||
defp sanitise_target({:plug, module}), do: sanitise_target({:plug, module, []})
|
||||
|
||||
defp sanitise_target({:plug, module, _}) do
|
||||
|
@ -290,7 +309,14 @@ defmodule Wayfarer.Router do
|
|||
end
|
||||
end
|
||||
|
||||
defp sanitise_target({scheme, address, port}), do: sanitise_listener({scheme, address, port})
|
||||
defp sanitise_target({scheme, address, port, transport}) do
|
||||
with {:ok, scheme} <- sanitise_scheme(scheme),
|
||||
{:ok, address} <- sanitise_ip_address(address),
|
||||
{:ok, port} <- sanitise_port(port),
|
||||
{:ok, transport} <- sanitise_transport(transport) do
|
||||
{:ok, {scheme, address, port, transport}}
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitise_target(target),
|
||||
do: {:error, ArgumentError.exception(message: "Not a valid target: `#{inspect(target)}")}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
defmodule Wayfarer.Server do
|
||||
alias Spark.OptionsHelpers
|
||||
alias Spark.Options
|
||||
alias Wayfarer.{Dsl, Listener, Router, Server, Target}
|
||||
use GenServer
|
||||
require Logger
|
||||
|
@ -7,8 +7,9 @@ defmodule Wayfarer.Server do
|
|||
@callback child_spec(keyword()) :: Supervisor.child_spec()
|
||||
@callback start_link(keyword()) :: GenServer.on_start()
|
||||
|
||||
@scheme_type {:in, [:http, :https]}
|
||||
@scheme_type {:in, [:http, :https, :ws, :wss]}
|
||||
@port_type {:in, 0..0xFFFF}
|
||||
@transport_type {:in, [:http1, :http2, :auto]}
|
||||
@ip_type {:or,
|
||||
[
|
||||
{:tuple, [{:in, 0..0xFF}, {:in, 0..0xFF}, {:in, 0..0xFF}, {:in, 0..0xFF}]},
|
||||
|
@ -50,7 +51,7 @@ defmodule Wayfarer.Server do
|
|||
{:tuple,
|
||||
[
|
||||
{:tuple, [@scheme_type, @ip_type, @port_type]},
|
||||
{:tuple, [@scheme_type, @ip_type, @port_type]},
|
||||
{:tuple, [@scheme_type, @ip_type, @port_type, @transport_type]},
|
||||
{:list, :string},
|
||||
{:in, [:round_robin, :sticky, :random, :least_connections]}
|
||||
]}},
|
||||
|
@ -78,7 +79,7 @@ defmodule Wayfarer.Server do
|
|||
|
||||
## Options
|
||||
|
||||
#{OptionsHelpers.docs(@options_schema)}
|
||||
#{Options.docs(@options_schema)}
|
||||
"""
|
||||
|
||||
@type options :: keyword
|
||||
|
@ -117,13 +118,14 @@ defmodule Wayfarer.Server do
|
|||
|
||||
@doc false
|
||||
@spec target_status_change(
|
||||
{module, :http | :https, IP.Address.t(), :socket.port_number()},
|
||||
{module, :http | :https, IP.Address.t(), :socket.port_number(),
|
||||
:http1 | :http2 | :auto},
|
||||
Router.health()
|
||||
) :: :ok
|
||||
def target_status_change({module, scheme, address, port}, status) do
|
||||
def target_status_change({module, scheme, address, port, transport}, status) do
|
||||
GenServer.cast(
|
||||
{:via, Registry, {Wayfarer.Server.Registry, module}},
|
||||
{:target_status_change, scheme, address, port, status}
|
||||
{:target_status_change, scheme, address, port, transport, status}
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -145,7 +147,7 @@ defmodule Wayfarer.Server do
|
|||
@impl true
|
||||
@spec init(options) :: {:ok, map} | {:stop, any}
|
||||
def init(options) do
|
||||
with {:ok, options} <- OptionsHelpers.validate(options, @options_schema),
|
||||
with {:ok, options} <- Options.validate(options, @options_schema),
|
||||
{:ok, module} <- assert_is_server(options[:module]),
|
||||
listeners <- Keyword.get(options, :listeners, []),
|
||||
targets <- Keyword.get(options, :targets, []),
|
||||
|
@ -165,10 +167,10 @@ defmodule Wayfarer.Server do
|
|||
@doc false
|
||||
@impl true
|
||||
@spec handle_cast(any, map) :: {:noreply, map}
|
||||
def handle_cast({:target_status_change, scheme, address, port, status}, state) do
|
||||
def handle_cast({:target_status_change, scheme, address, port, transport, status}, state) do
|
||||
Router.update_target_health_status(
|
||||
state.routing_table,
|
||||
{scheme, IP.Address.to_tuple(address), port},
|
||||
{scheme, IP.Address.to_tuple(address), port, transport},
|
||||
status
|
||||
)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ defmodule Wayfarer.Server.Plug do
|
|||
Plug pipeline to handle inbound HTTP connections.
|
||||
"""
|
||||
|
||||
alias Wayfarer.{Router, Server.Proxy, Target.Selector}
|
||||
alias Wayfarer.{Router, Server.Proxy, Target.Selector, Telemetry}
|
||||
require Logger
|
||||
|
||||
import Plug.Conn
|
||||
|
@ -17,22 +17,57 @@ defmodule Wayfarer.Server.Plug do
|
|||
@doc false
|
||||
@impl true
|
||||
@spec call(Plug.Conn.t(), map) :: Plug.Conn.t()
|
||||
def call(conn, config) when is_atom(config.module) do
|
||||
conn = put_private(conn, :wayfarer, %{listener: config})
|
||||
def call(conn, config) do
|
||||
transport = get_transport(conn)
|
||||
|
||||
conn
|
||||
|> put_private(:wayfarer, %{listener: config, transport: transport})
|
||||
|> Telemetry.request_start()
|
||||
|> do_call(config)
|
||||
end
|
||||
|
||||
defp do_call(conn, config) when is_atom(config.module) do
|
||||
listener = {config.scheme, config.address, config.port}
|
||||
|
||||
with {:ok, targets} <- Router.find_healthy_targets(config.module, listener, conn.host),
|
||||
{:ok, targets, algorithm} <- split_targets_and_algorithms(targets),
|
||||
{:ok, target} <- Selector.choose(conn, targets, algorithm) do
|
||||
do_proxy(conn, target)
|
||||
conn
|
||||
|> Telemetry.request_routed(target, algorithm)
|
||||
|> do_proxy(target)
|
||||
else
|
||||
:error -> bad_gateway(conn)
|
||||
{:error, reason} -> internal_error(conn, reason)
|
||||
:error ->
|
||||
conn
|
||||
|> Telemetry.request_exception(:error, :target_not_found)
|
||||
|> bad_gateway()
|
||||
|
||||
{:error, reason} ->
|
||||
conn
|
||||
|> Telemetry.request_exception(:error, reason)
|
||||
|> internal_error(reason)
|
||||
end
|
||||
rescue
|
||||
exception ->
|
||||
conn
|
||||
|> Telemetry.request_exception(:exception, exception, __STACKTRACE__)
|
||||
|> internal_error(exception)
|
||||
catch
|
||||
reason ->
|
||||
conn
|
||||
|> Telemetry.request_exception(:throw, reason)
|
||||
|> internal_error(reason)
|
||||
|
||||
kind, reason ->
|
||||
conn
|
||||
|> Telemetry.request_exception(kind, reason)
|
||||
|> internal_error(reason)
|
||||
end
|
||||
|
||||
def call(conn, _config), do: bad_gateway(conn)
|
||||
defp do_call(conn, _config) do
|
||||
conn
|
||||
|> Telemetry.request_exception(:error, :unrecognised_request)
|
||||
|> bad_gateway()
|
||||
end
|
||||
|
||||
defp internal_error(conn, reason) do
|
||||
Logger.error("Internal error when routing proxy request: #{inspect(reason)}")
|
||||
|
@ -64,4 +99,12 @@ defmodule Wayfarer.Server.Plug do
|
|||
do: split_targets_and_algorithms(tail, [target | targets], algorithm)
|
||||
|
||||
defp split_targets_and_algorithms([], targets, algorithm), do: {:ok, targets, algorithm}
|
||||
|
||||
defp get_transport(%{adapter: {Bandit.Adapter, %{transport: %Bandit.HTTP1.Socket{}}}}),
|
||||
do: :http1
|
||||
|
||||
defp get_transport(%{adapter: {Bandit.Adapter, %{transport: %Bandit.HTTP2.Stream{}}}}),
|
||||
do: :http2
|
||||
|
||||
defp get_transport(_), do: :unknown
|
||||
end
|
||||
|
|
|
@ -4,9 +4,9 @@ defmodule Wayfarer.Server.Proxy do
|
|||
specific target.
|
||||
"""
|
||||
|
||||
alias Mint.HTTP
|
||||
alias Mint.{HTTP, HTTP1, HTTP2}
|
||||
alias Plug.Conn
|
||||
alias Wayfarer.{Router, Target.ActiveConnections, Target.TotalConnections}
|
||||
alias Wayfarer.{Router, Target.ActiveConnections, Target.TotalConnections, Telemetry}
|
||||
require Logger
|
||||
|
||||
@connect_timeout 5_000
|
||||
|
@ -16,27 +16,67 @@ defmodule Wayfarer.Server.Proxy do
|
|||
Convert the request conn into an HTTP request to the specified target.
|
||||
"""
|
||||
@spec request(Conn.t(), Router.target()) :: Conn.t()
|
||||
def request(conn, {scheme, address, port} = target) do
|
||||
with {:ok, mint} <-
|
||||
HTTP.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout),
|
||||
def request(conn, target) do
|
||||
with {:ok, mint} <- connect(conn, target),
|
||||
:ok <- ActiveConnections.connect(target),
|
||||
:ok <- TotalConnections.proxy_connect(target),
|
||||
{:ok, body, conn} <- read_request_body(conn),
|
||||
{:ok, mint, req} <- send_request(conn, mint, body),
|
||||
{:ok, conn, _mint} <- proxy_responses(conn, mint, req) do
|
||||
conn
|
||||
:ok <- TotalConnections.proxy_connect(target) do
|
||||
handle_request(mint, conn, target)
|
||||
else
|
||||
error -> handle_error(error, conn, target)
|
||||
error ->
|
||||
conn
|
||||
|> Telemetry.request_exception(:error, error)
|
||||
|> handle_error(error, target)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_error({:error, _, reason}, conn, target),
|
||||
do: handle_error({:error, reason}, conn, target)
|
||||
defp handle_request(mint, conn, {proto, _, _, _}) do
|
||||
if http1?(conn) && connection_wants_upgrade?(conn) && upgrade_is_websocket?(conn) do
|
||||
handle_websocket_request(mint, conn, proto)
|
||||
else
|
||||
handle_http_request(mint, conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_error({:error, reason}, conn, {scheme, address, port}) do
|
||||
Logger.error(
|
||||
"Proxy error [phase=#{connection_phase(conn)},ip=#{:inet.ntoa(address)},port=#{port},proto=#{scheme}]: #{message(reason)}"
|
||||
)
|
||||
defp handle_http_request(mint, conn) do
|
||||
with {:ok, mint, req} <- send_request(conn, mint),
|
||||
{:ok, mint, conn} <- stream_request_body(conn, mint, req),
|
||||
{:ok, conn, _mint} <- proxy_responses(conn, mint, req) do
|
||||
conn
|
||||
|> Telemetry.request_stop()
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_websocket_request(mint, conn, proto) do
|
||||
conn
|
||||
|> WebSockAdapter.upgrade(Wayfarer.Server.WebSocketProxy, {mint, conn, proto}, compress: true)
|
||||
|> Telemetry.request_upgraded()
|
||||
end
|
||||
|
||||
defp connect(conn, {:ws, address, port, transport}),
|
||||
do: connect(conn, {:http, address, port, transport})
|
||||
|
||||
defp connect(conn, {:wss, address, port, transport}),
|
||||
do: connect(conn, {:https, address, port, transport})
|
||||
|
||||
defp connect(conn, {scheme, address, port, :http1}) when is_tuple(address),
|
||||
do: HTTP1.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout)
|
||||
|
||||
defp connect(conn, {scheme, address, port, :http2}) when is_tuple(address),
|
||||
do: HTTP2.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout)
|
||||
|
||||
defp connect(conn, {scheme, address, port, :auto}) when is_tuple(address),
|
||||
do: HTTP.connect(scheme, address, port, hostname: conn.host, timeout: @connect_timeout)
|
||||
|
||||
defp handle_error(conn, {:error, _, reason}, target),
|
||||
do: handle_error(conn, {:error, reason}, target)
|
||||
|
||||
defp handle_error(conn, {:error, reason}, {scheme, address, port, transport}) do
|
||||
Logger.error(fn ->
|
||||
phase = connection_phase(conn)
|
||||
ip = :inet.ntoa(address)
|
||||
|
||||
"Proxy error [phase=#{phase},ip=#{ip},port=#{port},proto=#{scheme},trans=#{transport}]: #{message(reason)}"
|
||||
end)
|
||||
|
||||
if conn.halted || conn.state in [:sent, :chunked, :upgraded] do
|
||||
# Sadly there's not much more we can do here.
|
||||
|
@ -50,7 +90,54 @@ defmodule Wayfarer.Server.Proxy do
|
|||
end
|
||||
end
|
||||
|
||||
defp connection_phase(nil), do: "connect"
|
||||
defp http1?(%{private: %{wayfarer: %{transport: :http1}}}), do: true
|
||||
defp http1?(_), do: false
|
||||
|
||||
defp connection_wants_upgrade?(conn) do
|
||||
case Conn.get_req_header(conn, "connection") do
|
||||
["Upgrade"] ->
|
||||
true
|
||||
|
||||
["upgrade"] ->
|
||||
true
|
||||
|
||||
[] ->
|
||||
false
|
||||
|
||||
maybe ->
|
||||
maybe
|
||||
|> Enum.flat_map(&String.split(&1, ~r/,\s*/))
|
||||
|> Enum.map(fn chunk ->
|
||||
chunk
|
||||
|> String.trim()
|
||||
|> String.downcase()
|
||||
end)
|
||||
|> Enum.member?("upgrade")
|
||||
end
|
||||
end
|
||||
|
||||
defp upgrade_is_websocket?(conn) do
|
||||
case Conn.get_req_header(conn, "upgrade") do
|
||||
["websocket"] ->
|
||||
true
|
||||
|
||||
[] ->
|
||||
false
|
||||
|
||||
maybe ->
|
||||
maybe
|
||||
|> Enum.flat_map(&String.split(&1, ~r/,\s*/))
|
||||
|> Enum.map(fn chunk ->
|
||||
chunk
|
||||
|> String.trim()
|
||||
|> String.split("/")
|
||||
|> hd()
|
||||
|> String.downcase()
|
||||
end)
|
||||
|> Enum.member?("websocket")
|
||||
end
|
||||
end
|
||||
|
||||
defp connection_phase(conn) when conn.state in [:chunked, :upgraded], do: "stream"
|
||||
defp connection_phase(conn) when conn.halted, do: "done"
|
||||
defp connection_phase(conn) when conn.state == :sent, do: "done"
|
||||
|
@ -72,92 +159,163 @@ defmodule Wayfarer.Server.Proxy do
|
|||
receive do
|
||||
message ->
|
||||
case HTTP.stream(mint, message) do
|
||||
:unknown -> proxy_responses(conn, mint, req)
|
||||
{:ok, mint, responses} -> handle_responses(responses, conn, mint, req)
|
||||
{:error, _, reason, _} -> {:error, reason}
|
||||
:unknown ->
|
||||
proxy_responses(conn, mint, req)
|
||||
|
||||
{:ok, mint, responses} ->
|
||||
handle_responses(conn, responses, mint, req)
|
||||
|
||||
{:error, _, reason, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
after
|
||||
@idle_timeout -> {:error, conn, :idle_timeout}
|
||||
@idle_timeout -> {:error, :idle_timeout}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_responses([], conn, mint, req), do: proxy_responses(conn, mint, req)
|
||||
defp handle_responses(conn, [], mint, req), do: proxy_responses(conn, mint, req)
|
||||
|
||||
defp handle_responses([{:status, req, status} | responses], conn, mint, req),
|
||||
do: handle_responses(responses, Conn.put_status(conn, status), mint, req)
|
||||
|
||||
defp handle_responses([{:headers, req, headers} | responses], conn, mint, req) do
|
||||
conn =
|
||||
headers
|
||||
|> Enum.reduce(conn, &Conn.put_resp_header(&2, elem(&1, 0), elem(&1, 1)))
|
||||
|
||||
handle_responses(responses, conn, mint, req)
|
||||
defp handle_responses(conn, [{:status, req, status} | responses], mint, req) do
|
||||
conn
|
||||
|> Conn.put_status(status)
|
||||
|> Telemetry.request_received_status(status)
|
||||
|> handle_responses(responses, mint, req)
|
||||
end
|
||||
|
||||
defp handle_responses([{:data, req, body} | responses], conn, mint, req)
|
||||
defp handle_responses(conn, [{:headers, req, headers} | responses], mint, req) do
|
||||
headers
|
||||
|> Enum.reduce(conn, fn {header_name, header_value}, conn ->
|
||||
conn
|
||||
|> Conn.put_resp_header(header_name, header_value)
|
||||
end)
|
||||
|> handle_responses(responses, mint, req)
|
||||
end
|
||||
|
||||
defp handle_responses(conn, [{:data, req, body} | responses], mint, req)
|
||||
when conn.state == :chunked do
|
||||
case Conn.chunk(conn, body) do
|
||||
{:ok, conn} -> handle_responses(responses, conn, mint, req)
|
||||
{:error, reason} -> {:error, conn, reason}
|
||||
{:ok, conn} ->
|
||||
body_size = byte_size(body)
|
||||
|
||||
conn
|
||||
|> Telemetry.increment_metrics(%{resp_body_bytes: body_size})
|
||||
|> Telemetry.request_resp_body_chunk(body_size)
|
||||
|> handle_responses(responses, mint, req)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, conn, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_responses([{:data, req, body} | responses], conn, mint, req) do
|
||||
defp handle_responses(conn, [{:data, req, body} | responses], mint, req) do
|
||||
# We need to check here for a content-length or transfer encoding header and
|
||||
# deal with it. This should be refactored out into a proxy state rather
|
||||
# than using the conn as our state.
|
||||
|
||||
body_size = byte_size(body)
|
||||
|
||||
case Conn.get_resp_header(conn, "content-length") do
|
||||
[] ->
|
||||
conn = Conn.send_chunked(conn, conn.status)
|
||||
handle_responses([{:data, req, body} | responses], conn, mint, req)
|
||||
conn
|
||||
|> Conn.send_chunked(conn.status)
|
||||
|> Telemetry.request_resp_started()
|
||||
|> handle_responses([{:data, req, body} | responses], mint, req)
|
||||
|
||||
[length] ->
|
||||
if String.to_integer(length) == byte_size(body) do
|
||||
if String.to_integer(length) == body_size do
|
||||
conn =
|
||||
conn
|
||||
|> Conn.delete_resp_header("content-length")
|
||||
|> Conn.send_resp(conn.status, body)
|
||||
|> Telemetry.request_resp_started()
|
||||
|> Telemetry.increment_metrics(%{resp_body_bytes: body_size})
|
||||
|> Telemetry.request_resp_body_chunk(body_size)
|
||||
|> Conn.halt()
|
||||
|
||||
{:ok, conn, mint}
|
||||
else
|
||||
conn =
|
||||
conn
|
||||
|> Conn.delete_resp_header("content-length")
|
||||
|> Telemetry.increment_metrics(%{resp_body_bytes: body_size})
|
||||
|> Telemetry.request_resp_body_chunk(body_size)
|
||||
|> Conn.send_chunked(conn.status)
|
||||
|
||||
handle_responses([{:data, req, body} | responses], conn, mint, req)
|
||||
handle_responses(conn, [{:data, req, body} | responses], mint, req)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_responses([{:done, req} | _], conn, mint, req) do
|
||||
{:ok, Conn.halt(conn), mint}
|
||||
# If the connection is done without sending any body content, then we need to
|
||||
# send the respond and halt the conn.
|
||||
defp handle_responses(conn, [{:done, req} | _], mint, req) when conn.state == :unset do
|
||||
conn =
|
||||
conn
|
||||
|> Conn.send_resp(conn.status, "")
|
||||
|> Conn.halt()
|
||||
|
||||
{:ok, conn, mint}
|
||||
end
|
||||
|
||||
defp handle_responses([{:error, req, reason} | _], conn, _mint, req), do: {:error, conn, reason}
|
||||
defp handle_responses(conn, [{:done, req} | _], mint, req) when conn.state == :chunked,
|
||||
do: {:ok, Conn.halt(conn), mint}
|
||||
|
||||
# This is bad - we need to figure out how to stream the body, but it's fine
|
||||
# for now.
|
||||
defp read_request_body(conn, body \\ <<>>) do
|
||||
case Conn.read_body(conn) do
|
||||
{:ok, chunk, conn} -> {:ok, body <> chunk, conn}
|
||||
{:more, chunk, conn} -> read_request_body(conn, body <> chunk)
|
||||
{:error, reason} -> {:error, conn, reason}
|
||||
end
|
||||
end
|
||||
defp handle_responses(conn, [{:error, req, reason} | _], _mint, req), do: {:error, conn, reason}
|
||||
|
||||
defp send_request(conn, mint) do
|
||||
request_path =
|
||||
case {conn.request_path, conn.query_string} do
|
||||
{path, nil} -> path
|
||||
{path, ""} -> path
|
||||
{path, query} -> path <> "?" <> query
|
||||
end
|
||||
|
||||
defp send_request(conn, mint, body) do
|
||||
HTTP.request(
|
||||
mint,
|
||||
conn.method,
|
||||
conn.request_path,
|
||||
request_path,
|
||||
proxy_headers(conn),
|
||||
body
|
||||
:stream
|
||||
)
|
||||
end
|
||||
|
||||
defp stream_request_body(conn, mint, req) do
|
||||
case Conn.read_body(conn) do
|
||||
{:ok, <<>>, conn} ->
|
||||
with {:ok, mint} <- HTTP.stream_request_body(mint, req, :eof) do
|
||||
conn =
|
||||
conn
|
||||
|> Telemetry.set_metrics(%{req_body_bytes: 0, req_body_chunks: 0})
|
||||
|
||||
{:ok, mint, conn}
|
||||
end
|
||||
|
||||
{:ok, chunk, conn} ->
|
||||
with {:ok, mint} <- HTTP.stream_request_body(mint, req, chunk),
|
||||
{:ok, mint} <- HTTP.stream_request_body(mint, req, :eof) do
|
||||
chunk_size = byte_size(chunk)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> Telemetry.increment_metrics(%{req_body_bytes: chunk_size, req_body_chunks: 1})
|
||||
|> Telemetry.request_req_body_chunk(chunk_size)
|
||||
|
||||
{:ok, mint, conn}
|
||||
end
|
||||
|
||||
{:more, chunk, conn} ->
|
||||
with {:ok, mint} <- HTTP.stream_request_body(mint, req, chunk) do
|
||||
chunk_size = byte_size(chunk)
|
||||
|
||||
conn
|
||||
|> Telemetry.increment_metrics(%{req_body_bytes: chunk_size, req_body_chunks: 1})
|
||||
|> Telemetry.request_req_body_chunk(chunk_size)
|
||||
|> stream_request_body(mint, req)
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, conn, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_headers(conn) do
|
||||
listener =
|
||||
conn.private.wayfarer.listener
|
||||
|
|
256
lib/wayfarer/server/websocket_proxy.ex
Normal file
256
lib/wayfarer/server/websocket_proxy.ex
Normal file
|
@ -0,0 +1,256 @@
|
|||
defmodule Wayfarer.Server.WebSocketProxy do
|
||||
@moduledoc """
|
||||
When a connection is upgraded to a websocket, we switch from handing via
|
||||
`Plug` to `WebSock` via `WebSockAdapter`.
|
||||
|
||||
The outgoing connection is made using `Mint.WebSocket`.
|
||||
"""
|
||||
|
||||
@behaviour WebSock
|
||||
|
||||
alias Mint.WebSocket
|
||||
alias Plug.Conn
|
||||
alias Wayfarer.Telemetry
|
||||
require Logger
|
||||
|
||||
@default_opts [extensions: [WebSocket.PerMessageDeflate]]
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def init({mint, conn, proto}) when proto in [:ws, :wss] do
|
||||
request_path =
|
||||
case {conn.request_path, conn.query_string} do
|
||||
{path, nil} -> path
|
||||
{path, ""} -> path
|
||||
{path, query} -> path <> "?" <> query
|
||||
end
|
||||
|
||||
case WebSocket.upgrade(proto, mint, request_path, proxy_headers(conn), @default_opts) do
|
||||
{:ok, mint, ref} -> {:ok, %{mint: mint, ref: ref, status: :init, buffer: [], conn: conn}}
|
||||
{:error, _mint, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def init({mint, conn, :https}), do: init({mint, conn, :wss})
|
||||
def init({mint, conn, :http}), do: init({mint, conn, :ws})
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def handle_control({frame, [{:opcode, :ping}]}, state) do
|
||||
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, {:ping, frame}),
|
||||
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
|
||||
conn = request_client_frame(state.conn, {:ping, frame})
|
||||
|
||||
{:ok, %{state | websocket: websocket, mint: mint, conn: conn}}
|
||||
else
|
||||
error -> handle_error(error, state)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_control({frame, [{:opcode, frame_type}]}, state) do
|
||||
conn = request_client_frame(state.conn, {frame_type, frame})
|
||||
|
||||
{:ok, %{state | conn: conn}}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def handle_in({payload, [{:opcode, frame_type}]}, state) when state.status == :init do
|
||||
frame = {frame_type, payload}
|
||||
buffer = [frame | state.buffer]
|
||||
conn = request_client_frame(state.conn, frame)
|
||||
|
||||
{:ok, %{state | buffer: buffer, conn: conn}}
|
||||
end
|
||||
|
||||
def handle_in({payload, [{:opcode, frame_type}]}, state) do
|
||||
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, {frame_type, payload}),
|
||||
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
|
||||
conn = request_client_frame(state.conn, {frame_type, payload})
|
||||
|
||||
{:ok, %{state | websocket: websocket, mint: mint, conn: conn}}
|
||||
else
|
||||
error -> handle_error(error, state)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def handle_info(msg, state) when state.status == :init do
|
||||
with {:ok, mint, result} <- WebSocket.stream(state.mint, msg),
|
||||
{:ok, result} <- handle_upgrade_response(result, state.ref),
|
||||
{:ok, mint, websocket} <- WebSocket.new(mint, state.ref, result.status, result.headers),
|
||||
state <- Map.merge(state, %{status: :connected, websocket: websocket, mint: mint}),
|
||||
{:ok, state} <- empty_buffer(state),
|
||||
{:ok, messages, state} <- decode_frames(result.data, state) do
|
||||
response_for_messages(messages, state)
|
||||
else
|
||||
error -> handle_error(error, state)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(msg, state) when state.status == :connected do
|
||||
with {:ok, mint, result} <- WebSocket.stream(state.mint, msg),
|
||||
{:ok, frames} <- handle_websocket_data(result, state.ref),
|
||||
{:ok, messages, state} <- decode_frames(frames, %{state | mint: mint}) do
|
||||
response_for_messages(messages, state)
|
||||
else
|
||||
error -> handle_error(error, state)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def terminate(_reason, _state), do: :ok
|
||||
|
||||
defp handle_error({:error, _, %{reason: reason}, _}, state),
|
||||
do: handle_error({:error, reason}, state)
|
||||
|
||||
defp handle_error({:error, reason, state}, _state),
|
||||
do: handle_error({:error, reason}, state)
|
||||
|
||||
defp handle_error({:error, reason}, state) do
|
||||
Logger.debug(fn ->
|
||||
"Dropping WebSocket connection for reason: #{inspect(reason)}"
|
||||
end)
|
||||
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
defp proxy_headers(conn) do
|
||||
listener =
|
||||
conn.private.wayfarer.listener
|
||||
|> case do
|
||||
%{address: address, port: port} when tuple_size(address) == 8 ->
|
||||
"[#{:inet.ntoa(address)}]:#{port}"
|
||||
|
||||
%{address: address, port: port} ->
|
||||
"#{:inet.ntoa(address)}:#{port}"
|
||||
end
|
||||
|
||||
client =
|
||||
conn
|
||||
|> Conn.get_peer_data()
|
||||
|> case do
|
||||
%{address: address, port: port} when tuple_size(address) == 8 ->
|
||||
"[#{:inet.ntoa(address)}]:#{port}"
|
||||
|
||||
%{address: address, port: port} ->
|
||||
"#{:inet.ntoa(address)}:#{port}"
|
||||
end
|
||||
|
||||
req_headers =
|
||||
conn.req_headers
|
||||
|> Enum.reject(
|
||||
&(elem(&1, 0) in [
|
||||
"connection",
|
||||
"upgrade",
|
||||
"sec-websocket-extensions",
|
||||
"sec-websocket-key",
|
||||
"sec-websocket-version"
|
||||
])
|
||||
)
|
||||
|
||||
[
|
||||
{"forwarded", "by=#{listener};for=#{client};host=#{conn.host};proto=#{conn.scheme}"}
|
||||
| req_headers
|
||||
]
|
||||
end
|
||||
|
||||
defp empty_buffer(state) when state.buffer == [], do: {:ok, state}
|
||||
defp empty_buffer(state), do: do_empty_buffer(Enum.reverse(state.buffer), %{state | buffer: []})
|
||||
defp do_empty_buffer([], state), do: {:ok, state}
|
||||
|
||||
defp do_empty_buffer([head | tail], state) do
|
||||
with {:ok, websocket, data} <- WebSocket.encode(state.websocket, head),
|
||||
{:ok, mint} <- WebSocket.stream_request_body(state.mint, state.ref, data) do
|
||||
conn = request_client_frame(state.conn, head)
|
||||
|
||||
do_empty_buffer(tail, %{state | websocket: websocket, mint: mint, conn: conn})
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_upgrade_response(result, ref), do: handle_upgrade_response(result, ref, %{data: []})
|
||||
defp handle_upgrade_response([{:done, ref}], ref, result), do: {:ok, result}
|
||||
|
||||
defp handle_upgrade_response([{:status, ref, status} | tail], ref, result) do
|
||||
handle_upgrade_response(tail, ref, Map.put(result, :status, status))
|
||||
end
|
||||
|
||||
defp handle_upgrade_response([{:headers, ref, headers} | tail], ref, result) do
|
||||
handle_upgrade_response(tail, ref, Map.put(result, :headers, headers))
|
||||
end
|
||||
|
||||
defp handle_upgrade_response([{:data, ref, data} | tail], ref, result) do
|
||||
result = Map.update!(result, :data, &[data | &1])
|
||||
handle_upgrade_response(tail, ref, result)
|
||||
end
|
||||
|
||||
defp handle_websocket_data(data, ref),
|
||||
do: handle_websocket_data(data, ref, [])
|
||||
|
||||
defp handle_websocket_data([], _ref, messages), do: {:ok, Enum.reverse(messages)}
|
||||
|
||||
defp handle_websocket_data([{:data, ref, data} | tail], ref, messages),
|
||||
do: handle_websocket_data(tail, ref, [data | messages])
|
||||
|
||||
defp decode_frames(frames, state) do
|
||||
frames
|
||||
|> Enum.reduce_while({:ok, [], state}, fn frame, {:ok, messages, state} ->
|
||||
case decode_frame(state, frame) do
|
||||
{:ok, new_messages, state} -> {:cont, {:ok, [new_messages, messages], state}}
|
||||
{:error, reason, state} -> {:halt, {:error, reason, state}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, messages, state} -> {:ok, List.flatten(messages), state}
|
||||
{:error, reason, state} -> {:error, reason, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp decode_frame(state, frame) do
|
||||
case WebSocket.decode(state.websocket, frame) do
|
||||
{:ok, websocket, frames} when is_list(frames) ->
|
||||
conn = Enum.reduce(frames, state.conn, &request_server_frame(&2, &1))
|
||||
{:ok, frames, %{state | websocket: websocket, conn: conn}}
|
||||
|
||||
{:error, websocket, reason} ->
|
||||
{:error, reason, %{state | websocket: websocket}}
|
||||
end
|
||||
end
|
||||
|
||||
# Handle all the frames coming from the target and decide how to respond to
|
||||
# Bandit/WebSock. In the case of encountering a close frame, we terminate the
|
||||
# client websocket with the same code, otherwise we just copy the frames over.
|
||||
defp response_for_messages(messages, state, response \\ [])
|
||||
defp response_for_messages([], state, []), do: {:ok, state}
|
||||
defp response_for_messages([], state, response), do: {:push, Enum.reverse(response), state}
|
||||
|
||||
defp response_for_messages([{:close, code, _} | _], state, response),
|
||||
do: {:stop, :normal, code, Enum.reverse(response), state}
|
||||
|
||||
defp response_for_messages([message | messages], state, response),
|
||||
do: response_for_messages(messages, state, [message | response])
|
||||
|
||||
defp request_client_frame(conn, {frame_type, frame}) do
|
||||
frame_size = byte_size(frame)
|
||||
|
||||
conn
|
||||
|> Telemetry.increment_metrics(%{
|
||||
client_frame_bytes: frame_size,
|
||||
client_frame_count: 1
|
||||
})
|
||||
|> Telemetry.request_client_frame(frame_size, frame_type)
|
||||
end
|
||||
|
||||
defp request_server_frame(conn, {frame_type, frame}) do
|
||||
frame_size = byte_size(frame)
|
||||
|
||||
conn
|
||||
|> Telemetry.increment_metrics(%{
|
||||
server_frame_bytes: frame_size,
|
||||
server_frame_count: 1
|
||||
})
|
||||
|> Telemetry.request_server_frame(frame_size, frame_type)
|
||||
end
|
||||
end
|
|
@ -3,14 +3,14 @@ defmodule Wayfarer.Target do
|
|||
|
||||
use GenServer, restart: :transient
|
||||
require Logger
|
||||
alias Spark.OptionsHelpers
|
||||
alias Spark.Options
|
||||
alias Wayfarer.{Dsl.HealthCheck, Router, Server, Target}
|
||||
import Wayfarer.Utils
|
||||
|
||||
@options_schema [
|
||||
scheme: [
|
||||
type: {:in, [:http, :https]},
|
||||
doc: "The connection protocol.",
|
||||
type: {:in, [:http, :https, :ws, :wss]},
|
||||
doc: "The connection scheme.",
|
||||
required: true
|
||||
],
|
||||
port: [
|
||||
|
@ -33,6 +33,12 @@ defmodule Wayfarer.Target do
|
|||
doc: "An optional name for the target.",
|
||||
required: false
|
||||
],
|
||||
transport: [
|
||||
type: {:in, [:http1, :http2, :auto]},
|
||||
required: false,
|
||||
default: :auto,
|
||||
doc: "The connection protocol."
|
||||
],
|
||||
health_checks: [
|
||||
type: {:list, {:keyword_list, HealthCheck.schema()}},
|
||||
required: false,
|
||||
|
@ -61,7 +67,7 @@ defmodule Wayfarer.Target do
|
|||
|
||||
## Options
|
||||
|
||||
#{OptionsHelpers.docs(@options_schema)}
|
||||
#{Options.docs(@options_schema)}
|
||||
"""
|
||||
|
||||
@type key :: {module, :http | :https, IP.Address.t(), :socket.port_number()}
|
||||
|
@ -90,9 +96,9 @@ defmodule Wayfarer.Target do
|
|||
@doc false
|
||||
@impl true
|
||||
def init(options) do
|
||||
with {:ok, options} <- OptionsHelpers.validate(options, @options_schema),
|
||||
with {:ok, options} <- Options.validate(options, @options_schema),
|
||||
{:ok, uri} <- to_uri(options[:scheme], options[:address], options[:port]) do
|
||||
target = options |> Keyword.take(~w[scheme address port]a) |> Map.new()
|
||||
target = options |> Keyword.take(~w[scheme address port transport]a) |> Map.new()
|
||||
module = options[:module]
|
||||
|
||||
key = {module, target.scheme, target.address, target.port}
|
||||
|
@ -117,6 +123,7 @@ defmodule Wayfarer.Target do
|
|||
method: check[:method] |> to_string() |> String.upcase(),
|
||||
headers: @default_headers,
|
||||
hostname: check[:hostname] || uri.host,
|
||||
transport: target.transport,
|
||||
passes: 0
|
||||
})
|
||||
|
||||
|
@ -170,7 +177,8 @@ defmodule Wayfarer.Target do
|
|||
end)
|
||||
|
||||
Server.target_status_change(
|
||||
{state.module, state.target.scheme, state.target.address, state.target.port},
|
||||
{state.module, state.target.scheme, state.target.address, state.target.port,
|
||||
state.target.transport},
|
||||
:unhealthy
|
||||
)
|
||||
|
||||
|
@ -205,7 +213,8 @@ defmodule Wayfarer.Target do
|
|||
|
||||
if target_became_healthy? do
|
||||
Server.target_status_change(
|
||||
{state.module, state.target.scheme, state.target.address, state.target.port},
|
||||
{state.module, state.target.scheme, state.target.address, state.target.port,
|
||||
state.target.transport},
|
||||
:healthy
|
||||
)
|
||||
|
||||
|
|
|
@ -36,8 +36,8 @@ defmodule Wayfarer.Target.ActiveConnections do
|
|||
@impl true
|
||||
@spec handle_info(:tick | {:DOWN, any, :process, any, pid, any}, state) :: {:noreply, state}
|
||||
def handle_info(:tick, state) do
|
||||
size = :ets.info(state.table, :size)
|
||||
Logger.debug("Active connections: #{size}")
|
||||
# size = :ets.info(state.table, :size)
|
||||
# Logger.debug("Active connections: #{size}")
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
|
|
@ -4,23 +4,48 @@ defmodule Wayfarer.Target.Check do
|
|||
"""
|
||||
|
||||
use GenServer, restart: :transient
|
||||
alias Mint.HTTP
|
||||
alias Wayfarer.{Target, Target.TotalConnections}
|
||||
alias Mint.{HTTP, HTTP1, HTTP2, WebSocket}
|
||||
alias Wayfarer.{Target, Target.TotalConnections, Telemetry}
|
||||
require Logger
|
||||
|
||||
@type state :: %{
|
||||
conn: struct(),
|
||||
req: reference(),
|
||||
scheme: :http | :https | :ws | :wss,
|
||||
address: :inet.ip_address(),
|
||||
port: :socket.port_number(),
|
||||
uri: URI.t(),
|
||||
ref: any,
|
||||
method: String.t(),
|
||||
headers: [{String.t(), String.t()}],
|
||||
hostname: String.t(),
|
||||
transport: :http1 | :http2 | :auto,
|
||||
span: map
|
||||
}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
def init(state), do: {:ok, state, {:continue, :start_check}}
|
||||
|
||||
@doc false
|
||||
@impl true
|
||||
@spec handle_continue(:start_check, state) :: {:noreply, state, timeout} | {:stop, :normal, nil}
|
||||
def handle_continue(:start_check, state) do
|
||||
with {:ok, conn} <- connect(state),
|
||||
{:ok, conn, req} <- request(Map.put(state, :conn, conn)) do
|
||||
state =
|
||||
state
|
||||
|> Map.merge(%{conn: conn, req: req})
|
||||
state =
|
||||
state
|
||||
|> Map.put(:span, %{
|
||||
metadata: %{
|
||||
target: %{scheme: state.scheme, address: state.address, port: state.port},
|
||||
method: state.method,
|
||||
uri: state.uri,
|
||||
hostname: state.hostname,
|
||||
telemetry_span_context: make_ref()
|
||||
}
|
||||
})
|
||||
|> Telemetry.health_check_start()
|
||||
|
||||
with {:ok, state} <- connect(state),
|
||||
{:ok, state} <- request(state) do
|
||||
{:noreply, state, state.response_timeout}
|
||||
else
|
||||
{:error, reason} ->
|
||||
|
@ -33,59 +58,121 @@ defmodule Wayfarer.Target.Check do
|
|||
|
||||
@doc false
|
||||
@impl true
|
||||
def handle_info(:timeout, state), do: check_failed(state, "request timeout expired.")
|
||||
def handle_info(:timeout, state), do: check_failed(state, :timeout)
|
||||
|
||||
def handle_info(message, state) do
|
||||
with {:ok, conn, responses} <- Mint.HTTP.stream(state.conn, message),
|
||||
:ok <- TotalConnections.health_check_connect({state.scheme, state.address, state.port}),
|
||||
with {:ok, conn, responses} <- WebSocket.stream(state.conn, message),
|
||||
:ok <-
|
||||
TotalConnections.health_check_connect(
|
||||
{state.scheme, state.address, state.port, state.transport}
|
||||
),
|
||||
{:ok, status} <- get_status_response(conn, responses) do
|
||||
if Enum.any?(state.success_codes, &Enum.member?(&1, status)) do
|
||||
Target.check_passed(state.ref)
|
||||
Telemetry.health_check_pass(state, status)
|
||||
{:stop, :normal, nil}
|
||||
else
|
||||
check_failed(state, "received #{status} status code")
|
||||
end
|
||||
else
|
||||
{:continue, conn} ->
|
||||
{:noreply, %{state | conn: conn}}
|
||||
{:noreply, Map.put(state, :conn, conn)}
|
||||
|
||||
:unknown ->
|
||||
check_failed(state, "Received unknown message: `#{inspect(message)}`")
|
||||
|
||||
{:error, reason} ->
|
||||
check_failed(state, reason)
|
||||
{:error, _conn, error, _responses} ->
|
||||
check_failed(state, error)
|
||||
end
|
||||
end
|
||||
|
||||
defp connect(state),
|
||||
do:
|
||||
HTTP.connect(state.scheme, state.address, state.port,
|
||||
timeout: state.connect_timeout,
|
||||
hostname: state.hostname
|
||||
)
|
||||
defp connect(state) when state.scheme == :ws,
|
||||
do: connect(%{state | scheme: :http})
|
||||
|
||||
defp request(state),
|
||||
do: HTTP.request(state.conn, state.method, state.path, state.headers, nil)
|
||||
defp connect(state) when state.scheme == :wss,
|
||||
do: connect(%{state | scheme: :https})
|
||||
|
||||
defp connect(state) when state.transport == :http1 do
|
||||
with {:ok, conn} <-
|
||||
HTTP1.connect(state.scheme, state.address, state.port,
|
||||
timeout: state.connect_timeout,
|
||||
hostname: state.hostname
|
||||
) do
|
||||
{:ok, Telemetry.health_check_connect(Map.put(state, :conn, conn), :http1)}
|
||||
end
|
||||
end
|
||||
|
||||
defp connect(state) when state.transport == :http2 do
|
||||
with {:ok, conn} <-
|
||||
HTTP2.connect(state.scheme, state.address, state.port,
|
||||
timeout: state.connect_timeout,
|
||||
hostname: state.hostname
|
||||
) do
|
||||
{:ok, Telemetry.health_check_connect(Map.put(state, :conn, conn), :http2)}
|
||||
end
|
||||
end
|
||||
|
||||
defp connect(state) do
|
||||
with {:ok, conn} <-
|
||||
HTTP.connect(state.scheme, state.address, state.port,
|
||||
timeout: state.connect_timeout,
|
||||
hostname: state.hostname
|
||||
) do
|
||||
transport =
|
||||
case conn do
|
||||
%Mint.HTTP1{} -> :http1
|
||||
%Mint.HTTP2{} -> :http2
|
||||
end
|
||||
|
||||
{:ok, Telemetry.health_check_connect(Map.put(state, :conn, conn), transport)}
|
||||
end
|
||||
end
|
||||
|
||||
defp request(state) when state.scheme in [:ws, :wss] do
|
||||
with {:ok, conn, req} <-
|
||||
WebSocket.upgrade(state.scheme, state.conn, state.path, state.headers, []) do
|
||||
state = Map.merge(state, %{conn: conn, req: req})
|
||||
|
||||
{:ok, Telemetry.health_check_request(state)}
|
||||
end
|
||||
end
|
||||
|
||||
defp request(state) do
|
||||
with {:ok, conn, req} <-
|
||||
HTTP.request(state.conn, state.method, state.path, state.headers, nil) do
|
||||
state = Map.merge(state, %{conn: conn, req: req})
|
||||
|
||||
{:ok, Telemetry.health_check_request(state)}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_failed(state, reason) when is_binary(reason) do
|
||||
Target.check_failed(state.ref)
|
||||
Logger.warning("Health check failed for #{state.method} #{state.uri}: #{reason}.")
|
||||
Telemetry.health_check_fail(state, reason)
|
||||
|
||||
Logger.warning(fn -> "Health check failed for #{state.method} #{state.uri}: #{reason}." end)
|
||||
{:stop, :normal, nil}
|
||||
end
|
||||
|
||||
defp check_failed(state, exception) when is_exception(exception) do
|
||||
Target.check_failed(state.ref)
|
||||
Telemetry.health_check_fail(state, exception)
|
||||
|
||||
Logger.warning(
|
||||
Logger.warning(fn ->
|
||||
"Health check failed for #{state.method} #{state.uri}: #{Exception.message(exception)}"
|
||||
)
|
||||
end)
|
||||
|
||||
{:stop, :normal, nil}
|
||||
end
|
||||
|
||||
defp check_failed(state, reason) do
|
||||
Target.check_failed(state.ref)
|
||||
Logger.warning("Health check failed for #{state.method} #{state.uri}: `#{inspect(reason)}`")
|
||||
Telemetry.health_check_fail(state, reason)
|
||||
|
||||
Logger.warning(fn ->
|
||||
"Health check failed for #{state.method} #{state.uri}: `#{inspect(reason)}`"
|
||||
end)
|
||||
|
||||
{:stop, :normal, nil}
|
||||
end
|
||||
|
||||
|
|
764
lib/wayfarer/telemetry.ex
Normal file
764
lib/wayfarer/telemetry.ex
Normal file
|
@ -0,0 +1,764 @@
|
|||
defmodule Wayfarer.Telemetry do
|
||||
@moduledoc """
|
||||
Wayfarer emits a number of telemetry events and spans on top of the excellent
|
||||
telemetry events emitted by `Bandit.Telemetry`.
|
||||
"""
|
||||
|
||||
alias Plug.Conn
|
||||
alias Wayfarer.{Router, Target.Check, Target.Selector}
|
||||
|
||||
@typedoc """
|
||||
Information about the target a request has been routed to.
|
||||
"""
|
||||
@type target ::
|
||||
%{
|
||||
scheme: :http | :https | :ws | :wss,
|
||||
address: :inet.ip_address(),
|
||||
port: :socket.port_number()
|
||||
}
|
||||
| %{scheme: :plug, module: module, options: any}
|
||||
|
||||
@typedoc """
|
||||
Information about the listener that received the request.
|
||||
"""
|
||||
@type listener ::
|
||||
%{
|
||||
scheme: :http | :https,
|
||||
module: module(),
|
||||
port: :socket.port_number(),
|
||||
address: :inet.ip_address()
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
The time that the event was emitted, in `:native` time units.
|
||||
|
||||
This is sources from `System.monotonic_time/0` which has some caveats but in
|
||||
general is better for calculating durations as it should never go backwards.
|
||||
"""
|
||||
@type monotonic_time :: integer()
|
||||
|
||||
@typedoc """
|
||||
The time passed since the beginning of the span, in `:native` time units.
|
||||
|
||||
The difference between the current `monotonic_time` and the first
|
||||
`monotonic_time` at the start of the span.
|
||||
"""
|
||||
@type duration :: integer()
|
||||
|
||||
@typedoc "The HTTP protocol version of the request"
|
||||
@type transport :: :http1 | :http2
|
||||
|
||||
@typedoc "A unique identifier for the span"
|
||||
@type telemetry_span_context :: reference()
|
||||
|
||||
@typedoc "A convenience type for describing telemetry events"
|
||||
@opaque event(measurements, metadata) :: {measurements, metadata}
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :start]` event.
|
||||
|
||||
This event signals the start of a request span tracking a client request to
|
||||
completion.
|
||||
|
||||
You can use the `telemetry_span_context` metadata value to correlate
|
||||
subsequent events within the same span.
|
||||
"""
|
||||
@type request_start ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:wallclock_time) => DateTime.t()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :routed]` event.
|
||||
|
||||
This event signals that the routing process has completed and a target has
|
||||
been chosen to serve the request.
|
||||
"""
|
||||
@type request_routed ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :exception]` event.
|
||||
|
||||
This event signals that something went wrong while processing the event. You
|
||||
will likely still receive other events (eg `:stop`) for this span however.
|
||||
"""
|
||||
@type request_exception ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
optional(:target) => target(),
|
||||
optional(:algorithm) => Selector.algorithm(),
|
||||
required(:kind) => :throw | :exit | :error | :exception,
|
||||
required(:reason) => any,
|
||||
required(:stacktrace) => Exception.stacktrace()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :stop]` event.
|
||||
|
||||
This event signals that the request has completed.
|
||||
|
||||
The measurements will contain any incrementing counters accumulated during the
|
||||
course of the request.
|
||||
"""
|
||||
@type request_stop ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:status) => nil | 100..599,
|
||||
optional(:req_body_bytes) => non_neg_integer(),
|
||||
optional(:resp_body_bytes) => non_neg_integer(),
|
||||
optional(:client_frame_bytes) => non_neg_integer(),
|
||||
optional(:client_frame_count) => non_neg_integer(),
|
||||
optional(:server_frame_bytes) => non_neg_integer(),
|
||||
optional(:server_frame_count) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
optional(:target) => target(),
|
||||
optional(:algorithm) => Selector.algorithm(),
|
||||
optional(:status) => nil | 100..599,
|
||||
optional(:kind) => :throw | :exit | :error | :exception,
|
||||
optional(:reason) => any,
|
||||
optional(:stacktrace) => Exception.stacktrace()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :received_status]` event.
|
||||
|
||||
This event signals that an HTTP status code has been received from the
|
||||
upstream target.
|
||||
"""
|
||||
@type request_received_status ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:status) => nil | 100..599,
|
||||
optional(:req_body_bytes) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :req_body_chunk]` event.
|
||||
|
||||
This event is emitted while streaming the request body from the client to the
|
||||
target. Under the hood `Plug.Conn.read_body/2` is being called with the
|
||||
default options, meaning that each chunk is likely to be up to 8MB in size.
|
||||
|
||||
If there is no request body then this event will not be emitted and the
|
||||
`req_body_bytes` and `req_body_chunks` counters will both be set to zero for
|
||||
this request.
|
||||
"""
|
||||
@type request_req_body_chunk ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:status) => nil | 100..599,
|
||||
required(:req_body_bytes) => non_neg_integer(),
|
||||
required(:req_body_chunks) => non_neg_integer(),
|
||||
required(:chunk_bytes) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :resp_started]` event.
|
||||
|
||||
This event indicates that the HTTP status and headers have been received from
|
||||
the target and the response will now start being sent to the target.
|
||||
"""
|
||||
@type request_resp_started ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:status) => nil | 100..599,
|
||||
optional(:req_body_bytes) => non_neg_integer(),
|
||||
optional(:req_body_chunks) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :resp_body_chunk]` event.
|
||||
|
||||
This event is emitted every time a chunk of response body is received from the
|
||||
target for streaming to the client. Under the hood, these are emitted every
|
||||
time `Mint.HTTP.stream/2` returns a data frame.
|
||||
"""
|
||||
@type request_resp_body_chunk ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
optional(:req_body_bytes) => non_neg_integer(),
|
||||
optional(:req_body_chunks) => non_neg_integer(),
|
||||
required(:resp_body_bytes) => non_neg_integer(),
|
||||
required(:resp_body_chunks) => non_neg_integer(),
|
||||
required(:chunk_bytes) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :upgraded]` event.
|
||||
|
||||
This event is emitted when a client connection is upgraded to a WebSocket
|
||||
connection.
|
||||
"""
|
||||
@type request_upgraded ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :client_frame]` event.
|
||||
|
||||
This event is emitted any time a WebSocket frame is received from the client
|
||||
for transmission to the target.
|
||||
"""
|
||||
@type request_client_frame ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:frame_size) => non_neg_integer(),
|
||||
required(:client_frame_bytes) => non_neg_integer(),
|
||||
required(:client_frame_count) => non_neg_integer(),
|
||||
optional(:server_frame_bytes) => non_neg_integer(),
|
||||
optional(:server_frame_count) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599,
|
||||
required(:opcode) => :text | :binary | :ping | :pong | :close
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :request, :server_frame]` event.
|
||||
|
||||
This event is emitted any time a WebSocket frame is received from the target
|
||||
for transmission to the client.
|
||||
"""
|
||||
@type request_server_frame ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:duration) => duration(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:frame_size) => non_neg_integer(),
|
||||
required(:server_frame_bytes) => non_neg_integer(),
|
||||
required(:server_frame_count) => non_neg_integer(),
|
||||
optional(:client_frame_bytes) => non_neg_integer(),
|
||||
optional(:client_frame_count) => non_neg_integer()
|
||||
},
|
||||
%{
|
||||
required(:conn) => Conn.t(),
|
||||
required(:listener) => listener(),
|
||||
required(:transport) => transport(),
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:target) => target(),
|
||||
required(:algorithm) => Selector.algorithm(),
|
||||
required(:status) => nil | 100..599
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
All the event types that make up the `[:wayfarer, :request, :*]` span.
|
||||
"""
|
||||
@type request_span ::
|
||||
request_start
|
||||
| request_routed
|
||||
| request_exception
|
||||
| request_stop
|
||||
| request_received_status
|
||||
| request_req_body_chunk
|
||||
| request_resp_started
|
||||
| request_resp_body_chunk
|
||||
| request_upgraded
|
||||
| request_client_frame
|
||||
| request_server_frame
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :health_check, :start]` event.
|
||||
|
||||
This event signals the start of a health check span.
|
||||
|
||||
You can use the `telemetry_span_context` metadata value to correlate
|
||||
subsequent events within the same span.
|
||||
"""
|
||||
@type health_check_start ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:wallclock_time) => DateTime.t()
|
||||
},
|
||||
%{
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:hostname) => String.t(),
|
||||
required(:uri) => URI.t(),
|
||||
required(:target) => target(),
|
||||
required(:method) => String.t()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :health_check, :connect]` event.
|
||||
|
||||
This event signals that the outgoing TCP connection has been made to the
|
||||
target.
|
||||
"""
|
||||
@type health_check_connect ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:duration) => duration()
|
||||
},
|
||||
%{
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:hostname) => String.t(),
|
||||
required(:uri) => URI.t(),
|
||||
required(:target) => target(),
|
||||
required(:method) => String.t(),
|
||||
required(:transport) => transport()
|
||||
}
|
||||
)
|
||||
@typedoc """
|
||||
The `[:wayfarer, :health_check, :request]` event.
|
||||
|
||||
This event signals that the HTTP or WebSocket request has been sent.
|
||||
"""
|
||||
@type health_check_request ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:duration) => duration()
|
||||
},
|
||||
%{
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:hostname) => String.t(),
|
||||
required(:uri) => URI.t(),
|
||||
required(:target) => target(),
|
||||
required(:method) => String.t(),
|
||||
required(:transport) => transport()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :health_check, :pass]` event.
|
||||
|
||||
This event signals that the HTTP status code returned by the target matches
|
||||
one of the configured success codes.
|
||||
|
||||
It also signals the end of the span.
|
||||
"""
|
||||
@type health_check_pass ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:duration) => duration(),
|
||||
required(:status) => 100..599
|
||||
},
|
||||
%{
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:hostname) => String.t(),
|
||||
required(:uri) => URI.t(),
|
||||
required(:target) => target(),
|
||||
required(:method) => String.t(),
|
||||
required(:transport) => transport()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
The `[:wayfarer, :health_check, :fail]` event.
|
||||
|
||||
This event signals that the HTTP status code returned by the target did not
|
||||
match any of the configured success codes.
|
||||
|
||||
It also signals the end of the span.
|
||||
"""
|
||||
@type health_check_fail ::
|
||||
event(
|
||||
%{
|
||||
required(:monotonic_time) => monotonic_time(),
|
||||
required(:wallclock_time) => DateTime.t(),
|
||||
required(:duration) => duration(),
|
||||
required(:reason) => any
|
||||
},
|
||||
%{
|
||||
required(:telemetry_span_context) => telemetry_span_context(),
|
||||
required(:hostname) => String.t(),
|
||||
required(:uri) => URI.t(),
|
||||
required(:target) => target(),
|
||||
required(:method) => String.t(),
|
||||
required(:transport) => transport()
|
||||
}
|
||||
)
|
||||
|
||||
@typedoc """
|
||||
All the event types that make up the `[:wayfarer, :health_check, :*]` span.
|
||||
"""
|
||||
@type health_check_span ::
|
||||
health_check_start | health_check_request | health_check_pass | health_check_fail
|
||||
|
||||
@typedoc """
|
||||
All the spans that can be emitted by Wayfarer.
|
||||
"""
|
||||
@type spans :: request_span | health_check_span
|
||||
|
||||
@doc false
|
||||
@spec request_start(Conn.t()) :: Conn.t()
|
||||
def request_start(conn) do
|
||||
telemetry_span_context = make_ref()
|
||||
|
||||
metadata =
|
||||
conn
|
||||
|> Map.get(:private, %{})
|
||||
|> Map.get(:wayfarer, %{})
|
||||
|> Map.merge(%{telemetry_span_context: telemetry_span_context, conn: conn})
|
||||
|
||||
conn
|
||||
|> execute_request_span_event(:start, %{}, metadata)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_routed(Conn.t(), Router.target(), Router.algorithm()) :: Conn.t()
|
||||
def request_routed(conn, target, algorithm) do
|
||||
target =
|
||||
case target do
|
||||
{scheme, address, port, _transport} -> %{scheme: scheme, address: address, port: port}
|
||||
{:plug, {module, opts}} -> %{scheme: :plug, module: module, options: opts}
|
||||
{:plug, module} -> %{scheme: :plug, module: module, options: []}
|
||||
end
|
||||
|
||||
conn
|
||||
|> execute_request_span_event(:routed, %{}, %{
|
||||
target: target,
|
||||
algorithm: algorithm
|
||||
})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_exception(Conn.t(), kind :: any, reason :: any, stacktrace :: list | nil) ::
|
||||
Conn.t()
|
||||
def request_exception(conn, kind, reason, stacktrace \\ nil) do
|
||||
conn
|
||||
|> execute_request_span_event(:exception, %{}, %{
|
||||
kind: kind,
|
||||
reason: reason,
|
||||
stacktrace: stacktrace || Process.info(self(), :current_stacktrace)
|
||||
})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def request_stop(conn) do
|
||||
conn
|
||||
|> execute_request_span_event(:stop, %{status: conn.status}, %{})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_received_status(Conn.t(), non_neg_integer()) :: Conn.t()
|
||||
def request_received_status(conn, status) do
|
||||
conn
|
||||
|> execute_request_span_event(:received_status, %{status: status}, %{status: status})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_req_body_chunk(Conn.t(), non_neg_integer()) :: Conn.t()
|
||||
def request_req_body_chunk(conn, chunk_bytes) do
|
||||
conn
|
||||
|> execute_request_span_event(:req_body_chunk, %{chunk_bytes: chunk_bytes}, %{})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_resp_started(Conn.t()) :: Conn.t()
|
||||
def request_resp_started(conn) do
|
||||
metric = %{status: conn.status}
|
||||
|
||||
conn
|
||||
|> execute_request_span_event(:resp_started, metric, metric)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_resp_body_chunk(Conn.t(), non_neg_integer()) :: Conn.t()
|
||||
def request_resp_body_chunk(conn, chunk_bytes) do
|
||||
conn
|
||||
|> execute_request_span_event(:resp_body_chunk, %{chunk_bytes: chunk_bytes}, %{})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_upgraded(Conn.t()) :: Conn.t()
|
||||
def request_upgraded(conn) do
|
||||
conn
|
||||
|> execute_request_span_event(:upgraded, %{}, %{})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_client_frame(Conn.t(), non_neg_integer(), atom) :: Conn.t()
|
||||
def request_client_frame(conn, bytes, opcode) do
|
||||
conn
|
||||
|> execute_request_span_event(:client_frame, %{frame_size: bytes}, %{opcode: opcode})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec request_server_frame(Conn.t(), non_neg_integer(), atom) :: Conn.t()
|
||||
def request_server_frame(conn, bytes, opcode) do
|
||||
conn
|
||||
|> execute_request_span_event(:server_frame, %{frame_size: bytes}, %{opcode: opcode})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec set_metrics(Conn.t(), %{atom => number}) :: Conn.t()
|
||||
def set_metrics(conn, metrics) do
|
||||
update_metrics(conn, &Map.merge(&1, metrics))
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec increment_metrics(Conn.t(), %{atom => number}) :: Conn.t()
|
||||
def increment_metrics(conn, to_increment) do
|
||||
update_metrics(conn, fn metrics ->
|
||||
Enum.reduce(to_increment, metrics, fn {metric_name, increment_by}, metrics ->
|
||||
Map.update(metrics, metric_name, increment_by, &(&1 + increment_by))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec health_check_start(Check.state()) :: Check.state()
|
||||
def health_check_start(check) do
|
||||
execute_check_span_event(check, :start, %{}, %{})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec health_check_connect(Check.state(), atom) :: Check.state()
|
||||
def health_check_connect(check, transport) do
|
||||
execute_check_span_event(check, :connect, %{}, %{transport: transport})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec health_check_request(Check.state()) :: Check.state()
|
||||
def health_check_request(check) do
|
||||
execute_check_span_event(check, :request, %{}, %{})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec health_check_fail(Check.state(), any) :: Check.state()
|
||||
def health_check_fail(check, reason) do
|
||||
execute_check_span_event(check, :fail, %{}, %{reason: reason})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec health_check_pass(Check.state(), 100..599) :: Check.state()
|
||||
def health_check_pass(check, status) do
|
||||
execute_check_span_event(check, :pass, %{}, %{status: status})
|
||||
end
|
||||
|
||||
defp update_metrics(conn, callback) do
|
||||
private =
|
||||
conn
|
||||
|> Map.get(:private, %{})
|
||||
|> Map.get(:wayfarer, %{})
|
||||
|
||||
metrics =
|
||||
private
|
||||
|> Map.get(:metrics, %{})
|
||||
|> callback.()
|
||||
|
||||
private = Map.put(private, :metrics, metrics)
|
||||
|
||||
conn
|
||||
|> Conn.put_private(:wayfarer, private)
|
||||
end
|
||||
|
||||
defp execute_request_span_event(conn, event, measurements, metadata) do
|
||||
monotonic_time = System.monotonic_time()
|
||||
now = DateTime.utc_now()
|
||||
|
||||
private =
|
||||
conn
|
||||
|> Map.get(:private, %{})
|
||||
|> Map.get(:wayfarer, %{})
|
||||
|
||||
span_info =
|
||||
private
|
||||
|> Map.get(:request_span, %{})
|
||||
|
||||
metrics =
|
||||
private
|
||||
|> Map.get(:metrics, %{})
|
||||
|
||||
measurements =
|
||||
if Map.has_key?(span_info, :start_time) do
|
||||
%{
|
||||
monotonic_time: monotonic_time,
|
||||
duration: monotonic_time - span_info.start_time,
|
||||
wallclock_time: now
|
||||
}
|
||||
else
|
||||
%{
|
||||
monotonic_time: monotonic_time,
|
||||
wallclock_time: now
|
||||
}
|
||||
end
|
||||
|> Map.merge(metrics)
|
||||
|> Map.merge(measurements)
|
||||
|
||||
metadata =
|
||||
span_info
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.merge(metadata)
|
||||
|
||||
:telemetry.execute([:wayfarer, :request, event], measurements, Map.put(metadata, :conn, conn))
|
||||
|
||||
span_info =
|
||||
span_info
|
||||
|> Map.put(:metadata, metadata)
|
||||
|> Map.put_new(:start_time, monotonic_time)
|
||||
|
||||
private =
|
||||
private
|
||||
|> Map.put(:request_span, span_info)
|
||||
|
||||
conn
|
||||
|> Conn.put_private(:wayfarer, private)
|
||||
end
|
||||
|
||||
defp execute_check_span_event(check, event, measurements, metadata) do
|
||||
monotonic_time = System.monotonic_time()
|
||||
now = DateTime.utc_now()
|
||||
span_info = Map.get(check, :span, %{})
|
||||
|
||||
metrics = Map.get(span_info, :metrics, %{})
|
||||
|
||||
measurements =
|
||||
if Map.has_key?(span_info, :start_time) do
|
||||
%{
|
||||
monotonic_time: monotonic_time,
|
||||
duration: monotonic_time - span_info.start_time,
|
||||
wallclock_time: now
|
||||
}
|
||||
else
|
||||
%{
|
||||
monotonic_time: monotonic_time,
|
||||
wallclock_time: now
|
||||
}
|
||||
end
|
||||
|> Map.merge(metrics)
|
||||
|> Map.merge(measurements)
|
||||
|
||||
metadata =
|
||||
span_info
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.merge(metadata)
|
||||
|
||||
:telemetry.execute([:wayfarer, :health_check, event], measurements, metadata)
|
||||
|
||||
span_info =
|
||||
span_info
|
||||
|> Map.put(:metadata, metadata)
|
||||
|> Map.put_new(:start_time, monotonic_time)
|
||||
|
||||
Map.put(check, :span, span_info)
|
||||
end
|
||||
end
|
|
@ -5,7 +5,8 @@ defmodule Wayfarer.Utils do
|
|||
|
||||
@type address_input :: IP.Address.t() | String.t() | :inet.ip_address()
|
||||
@type port_number :: 1..0xFFFF
|
||||
@type scheme :: :http | :https
|
||||
@type scheme :: :http | :https | :ws | :wss
|
||||
@type transport :: :http1 | :http2 | :auto
|
||||
|
||||
@doc """
|
||||
Verify an IP address and convert it into a tuple.
|
||||
|
@ -46,7 +47,7 @@ defmodule Wayfarer.Utils do
|
|||
Verify a scheme.
|
||||
"""
|
||||
@spec sanitise_scheme(scheme) :: {:ok, scheme} | {:error, any}
|
||||
def sanitise_scheme(scheme) when scheme in [:http, :https], do: {:ok, scheme}
|
||||
def sanitise_scheme(scheme) when scheme in [:http, :https, :ws, :wss], do: {:ok, scheme}
|
||||
|
||||
def sanitise_scheme(scheme),
|
||||
do:
|
||||
|
@ -58,7 +59,7 @@ defmodule Wayfarer.Utils do
|
|||
@doc """
|
||||
Convert a scheme, address, port tuple into a `URI`.
|
||||
"""
|
||||
@spec to_uri(:http | :https, address_input, port_number) :: {:ok, URI.t()} | {:error, any}
|
||||
@spec to_uri(scheme, address_input, port_number) :: {:ok, URI.t()} | {:error, any}
|
||||
def to_uri(scheme, address, port) do
|
||||
with {:ok, scheme} <- sanitise_scheme(scheme),
|
||||
{:ok, address} <- sanitise_ip_address(address),
|
||||
|
@ -76,7 +77,7 @@ defmodule Wayfarer.Utils do
|
|||
@doc """
|
||||
Convert a list of targets into a match spec guard.
|
||||
"""
|
||||
@spec targets_to_ms_guard(atom, [{:http | :https, address_input, port_number}]) :: [
|
||||
@spec targets_to_ms_guard(atom, [{scheme, :inet.ip_address(), port_number, transport}]) :: [
|
||||
{atom, any, any}
|
||||
]
|
||||
def targets_to_ms_guard(_var, []), do: []
|
||||
|
@ -92,11 +93,8 @@ defmodule Wayfarer.Utils do
|
|||
@doc """
|
||||
Convert a target tuple into a tuple safe for injection into a match spec.
|
||||
"""
|
||||
@spec target_to_ms({:http | :https, address_input, port_number}) ::
|
||||
{{:http | :https, address_input, port_number}}
|
||||
| {{:http | :https, {:inet.ip_address()}, port_number}}
|
||||
def target_to_ms({scheme, address, port}) when is_tuple(address),
|
||||
do: {{scheme, {address}, port}}
|
||||
|
||||
def target_to_ms({scheme, address, port}), do: {{scheme, address, port}}
|
||||
@spec target_to_ms({scheme, :inet.ip_address(), port_number, transport}) ::
|
||||
{{scheme, {:inet.ip_address()}, port_number, transport}}
|
||||
def target_to_ms({scheme, address, port, transport}) when is_tuple(address),
|
||||
do: {{scheme, {address}, port, transport}}
|
||||
end
|
||||
|
|
26
mix.exs
26
mix.exs
|
@ -5,7 +5,7 @@ defmodule Wayfarer.MixProject do
|
|||
A runtime-configurable HTTP reverse proxy based on Bandit.
|
||||
"""
|
||||
|
||||
@version "0.4.0"
|
||||
@version "0.6.1"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
@ -18,17 +18,16 @@ defmodule Wayfarer.MixProject do
|
|||
deps: deps(),
|
||||
description: @moduledoc,
|
||||
package: package(),
|
||||
source_url: "https://code.harton.nz/james/wayfarer",
|
||||
homepage_url: "https://code.harton.nz/james/wayfarer",
|
||||
source_url: "https://harton.dev/james/wayfarer",
|
||||
homepage_url: "https://harton.dev/james/wayfarer",
|
||||
aliases: aliases(),
|
||||
dialyzer: [plt_add_apps: []],
|
||||
dialyzer: [plt_ignore_apps: [:mint]],
|
||||
docs: [
|
||||
main: "Wayfarer",
|
||||
formatters: ["html"],
|
||||
extra_section: "GUIDES",
|
||||
filter_modules: ~r/^Elixir.Wayfarer/,
|
||||
source_url_pattern:
|
||||
"https://code.harton.nz/james/wayfarer/src/branch/main/%{path}#L%{line}",
|
||||
source_url_pattern: "https://harton.dev/james/wayfarer/src/branch/main/%{path}#L%{line}",
|
||||
spark: [
|
||||
extensions: [
|
||||
%{
|
||||
|
@ -71,9 +70,12 @@ defmodule Wayfarer.MixProject do
|
|||
maintainers: ["James Harton <james@harton.nz>"],
|
||||
licenses: ["HL3-FULL"],
|
||||
links: %{
|
||||
"Source" => "https://code.harton.nz/james/wayfarer"
|
||||
"Source" => "https://harton.dev/james/wayfarer",
|
||||
"GitHub" => "https://github.com/jimsynz/wayfarer",
|
||||
"Changelog" => "https://docs.harton.nz/james/wayfarer/changelog.html",
|
||||
"Sponsor" => "https://github.com/sponsors/jimsynz"
|
||||
},
|
||||
source_url: "https://code.harton.nz/james/wayfarer"
|
||||
source_url: "https://harton.dev/james/wayfarer"
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -94,20 +96,22 @@ defmodule Wayfarer.MixProject do
|
|||
{:castore, "~> 1.0"},
|
||||
{:ip, "~> 2.0"},
|
||||
{:mint, "~> 1.5"},
|
||||
{:mint_web_socket, "~> 1.0"},
|
||||
{:nimble_options, "~> 1.0"},
|
||||
{:plug, "~> 1.15"},
|
||||
{:spark, "~> 1.1"},
|
||||
{:spark, "~> 2.0"},
|
||||
{:telemetry, "~> 1.2"},
|
||||
{:websock, "~> 0.5"},
|
||||
{:websock_adapter, "~> 0.5"},
|
||||
|
||||
# Dev/test
|
||||
{:credo, "~> 1.7", opts},
|
||||
{:dialyxir, "~> 1.3", opts},
|
||||
{:doctor, "~> 0.21", opts},
|
||||
{:earmark, ">= 0.0.0", opts},
|
||||
{:ex_check, "~> 0.15", opts},
|
||||
{:ex_check, "~> 0.16", opts},
|
||||
{:ex_doc, ">= 0.0.0", opts},
|
||||
{:faker, "~> 0.17", opts},
|
||||
{:faker, "~> 0.18", opts},
|
||||
{:git_ops, "~> 2.6", opts},
|
||||
{:mimic, "~> 1.7", Keyword.delete(opts, :runtime)},
|
||||
{:mix_audit, "~> 2.1", opts}
|
||||
|
|
72
mix.lock
72
mix.lock
|
@ -1,39 +1,49 @@
|
|||
%{
|
||||
"bandit": {:hex, :bandit, "1.1.0", "1414e65916229d4ee0914f6d4e7f8ec16c6f2d90e01ad5174d89e90baa577625", [: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", "4891fb2f48a83445da70a4e949f649a9b4032310f1f640f4a8a372bc91cece18"},
|
||||
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
|
||||
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
|
||||
"credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [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", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"},
|
||||
"bandit": {:hex, :bandit, "1.5.7", "6856b1e1df4f2b0cb3df1377eab7891bec2da6a7fd69dc78594ad3e152363a50", [:mix], [{:hpax, "~> 1.0.0", [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", "f2dd92ae87d2cbea2fa9aa1652db157b6cba6c405cb44d4f6dd87abba41371cd"},
|
||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||
"castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
|
||||
"credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
|
||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.37", "2ad73550e27c8946648b06905a57e4d454e4d7229c2dafa72a0348c99d8be5f7", [:mix], [], "hexpm", "6b19783f2802f039806f375610faa22da130b8edc21209d0bff47918bb48360e"},
|
||||
"earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"},
|
||||
"earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"ex_check": {:hex, :ex_check, "0.15.0", "074b94c02de11c37bba1ca82ae5cc4926e6ccee862e57a485b6ba60fca2d8dc1", [:mix], [], "hexpm", "33848031a0c7e4209c3b4369ce154019788b5219956220c35ca5474299fb6a0e"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [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", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"},
|
||||
"faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"},
|
||||
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
|
||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||
"git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"},
|
||||
"git_ops": {:hex, :git_ops, "2.6.0", "e0791ee1cf5db03f2c61b7ebd70e2e95cba2bb9b9793011f26609f22c0900087", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "b98fca849b18aaf490f4ac7d1dd8c6c469b0cc3e6632562d366cab095e666ffe"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"ip": {:hex, :ip, "2.0.1", "c0b77da86e62cb28ae85132e84881bdb6e067be4a74de74a95db6637482b03e9", [:mix], [], "hexpm", "431d9cca05c4f835d592c3b1e59b44910e28437698e4f965119f50b78347b4fd"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
||||
"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"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||
"mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"},
|
||||
"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"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.1", "653aa6d8f291fc4b017aa82bdb79a4017903902ebba57960ef199cbbc8c008a1", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "541990c3ab3a7bb8c4aaa2ce2732a4ae160ad6237e5dcd5ad1564f4f85354db1"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
|
||||
"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"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||
"sourceror": {:hex, :sourceror, "0.14.0", "b6b8552d0240400d66b6f107c1bab7ac1726e998efc797f178b7b517e928e314", [:mix], [], "hexpm", "809c71270ad48092d40bbe251a133e49ae229433ce103f762a2373b7a10a8d8b"},
|
||||
"spark": {:hex, :spark, "1.1.48", "64b804711818526e371d12ea3acc886365b14239565e361001aad801a38bad85", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 0.1", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "3215a8b1bb1dc93945ce9a0f68430d7265ea596c6b911f7bd6dba77b65cee370"},
|
||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.1.0", "dcc115650adc61c5e7de12619f0cb94b2b8f050326e7f21ffbf6fdeb3d291e4c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7745cf71520d74e119827ff32c2da6307e822cf835bebed3b2c459cc57f32d21"},
|
||||
"git_ops": {:hex, :git_ops, "2.6.1", "cc7799a68c26cf814d6d1a5121415b4f5bf813de200908f930b27a2f1fe9dad5", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "ce62d07e41fe993ec22c35d5edb11cf333a21ddaead6f5d9868fcb607d42039e"},
|
||||
"glob_ex": {:hex, :glob_ex, "0.1.8", "f7ef872877ca2ae7a792ab1f9ff73d9c16bf46ecb028603a8a3c5283016adc07", [:mix], [], "hexpm", "9e39d01729419a60a937c9260a43981440c43aa4cadd1fa6672fecd58241c464"},
|
||||
"ham": {:hex, :ham, "0.3.0", "7cd031b4a55fba219c11553e7b13ba73bd86eab4034518445eff1e038cb9a44d", [:mix], [], "hexpm", "7d6c6b73d7a6a83233876cc1b06a4d9b5de05562b228effda4532f9a49852bf6"},
|
||||
"hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"},
|
||||
"igniter": {:hex, :igniter, "0.3.24", "791a91650ffab9d66b9a3011c66491f767577ad55c363f820cc188554207ee6f", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:owl, "~> 0.9", [hex: :owl, repo: "hexpm", optional: false]}, {:rewrite, "~> 0.9", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}, {:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: false]}], "hexpm", "2e1d336534c6129bae0db043fae650303b96974c0488c290191d6d4c61ec9a9f"},
|
||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||
"ip": {:hex, :ip, "2.0.3", "290d71c05b79ad62c99d8fe175e86130dc120489d119b8c2819cec16bad3c77c", [:mix], [], "hexpm", "19fa2f9c6f5cb288ca2192499888bd96f88af3564eaa7bbcfc1231ffdc5df8c2"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [: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", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
||||
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||
"mimic": {:hex, :mimic, "1.10.0", "58ee13aa46addcadbb033ce311bb5ed8b0a825c2dec6e1d55ca767138a6374c8", [:mix], [{:ham, "~> 0.2", [hex: :ham, repo: "hexpm", optional: false]}], "hexpm", "ea639e48f6a5bd043218297b80c3a52e227541aafa3dc8a299cc0c01991523a5"},
|
||||
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},
|
||||
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"},
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||
"owl": {:hex, :owl, "0.11.0", "2cd46185d330aa2400f1c8c3cddf8d2ff6320baeff23321d1810e58127082cae", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "73f5783f0e963cc04a061be717a0dbb3e49ae0c4bfd55fb4b78ece8d33a65efe"},
|
||||
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [: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", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||
"rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"},
|
||||
"sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"},
|
||||
"spark": {:hex, :spark, "2.2.23", "78f0a1b0b713a91ad556fe9dc19ec92d977aaa0803cce2e255d90e58b9045c2a", [:mix], [{:igniter, ">= 0.2.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "a354b5cd7c3f021e3cd1da5a033b7643fe7b3c71c96b96d9f500a742f40c94db"},
|
||||
"spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"},
|
||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
|
||||
"ucwidth": {:hex, :ucwidth, "0.2.0", "1f0a440f541d895dff142275b96355f7e91e15bca525d4a0cc788ea51f0e3441", [:mix], [], "hexpm", "c1efd1798b8eeb11fb2bec3cafa3dd9c0c3647bee020543f0340b996177355bf"},
|
||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
|
||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
|
||||
"yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"},
|
||||
}
|
||||
|
|
|
@ -4,25 +4,22 @@ defmodule Support.Example do
|
|||
|
||||
config "Example" do
|
||||
listeners do
|
||||
# http "127.0.0.1", 8080
|
||||
http "0.0.0.0", 8080
|
||||
http "0.0.0.0", 8000
|
||||
end
|
||||
|
||||
targets do
|
||||
http "127.0.0.1", 8082
|
||||
|
||||
http "192.168.4.26", 80
|
||||
http "127.0.0.1", 4000
|
||||
end
|
||||
|
||||
health_checks do
|
||||
check do
|
||||
interval :timer.seconds(5)
|
||||
success_codes 200..399
|
||||
end
|
||||
end
|
||||
|
||||
host_patterns do
|
||||
pattern "*.example.com"
|
||||
pattern "example.com"
|
||||
pattern "localhost"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,7 +48,7 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
assert :ok = Router.add_route(table, listener, target, ["*.example.com"], :round_robin)
|
||||
|
||||
|
@ -61,7 +61,7 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
assert :ok = Router.add_route(table, listener, target, ["www.example.com"], :round_robin)
|
||||
|
||||
|
@ -74,7 +74,7 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
assert :ok =
|
||||
Router.add_route(
|
||||
|
@ -114,9 +114,9 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
listener1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
assert :ok =
|
||||
Router.import_routes(table, [
|
||||
|
@ -142,9 +142,9 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
listener1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
Router.import_routes(table, [
|
||||
{listener0, target0, ["0.example.com"], :round_robin},
|
||||
|
@ -164,9 +164,9 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
listener1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
Router.import_routes(table, [
|
||||
{listener0, target0, ["0.example.com"], :round_robin},
|
||||
|
@ -186,9 +186,9 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
listener1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
Router.import_routes(table, [
|
||||
{listener0, target0, ["0.example.com"], :round_robin},
|
||||
|
@ -215,8 +215,8 @@ defmodule Wayfarer.RouterTest do
|
|||
{:ok, table} = Router.init(Support.Example)
|
||||
|
||||
listener = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target0 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
target1 = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
:ok =
|
||||
Router.import_routes(table, [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Wayfarer.Server.PlugTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
use Mimic
|
||||
use Plug.Test
|
||||
use Support.PortTracker
|
||||
|
@ -52,7 +52,7 @@ defmodule Wayfarer.Server.PlugTest do
|
|||
|
||||
test "it looks for healthy targets in the router" do
|
||||
listener = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
{:ok, _} = HttpServer.start_link(elem(target, 2), 200, "OK")
|
||||
|
||||
|
@ -76,7 +76,7 @@ defmodule Wayfarer.Server.PlugTest do
|
|||
|
||||
test "it selects a target to proxy to" do
|
||||
listener = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
{:ok, _} = HttpServer.start_link(elem(target, 2), 200, "OK")
|
||||
|
||||
|
|
|
@ -49,17 +49,18 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
|
||||
HTTP
|
||||
|> stub(:connect, fn _, _, _, _ -> {:ok, :fake_conn} end)
|
||||
|> stub(:stream_request_body, fn mint, _, _ -> {:ok, mint} end)
|
||||
|> stub(:request, fn mint, _, _, _, _ ->
|
||||
send(self(), :ignore)
|
||||
{:ok, mint, req}
|
||||
end)
|
||||
|> stub(:stream, fn mint, :ignore -> {:ok, mint, responses} end)
|
||||
|> stub(:stream, fn mint, _ -> {:ok, mint, [{:done, req}]} end)
|
||||
|> stub(:stream, fn mint, _ -> {:ok, mint, [{:status, req, 200}, {:done, req}]} end)
|
||||
end
|
||||
|
||||
describe "request/2" do
|
||||
test "it opens an HTTP connection to the target and sends the request", %{conn: conn} do
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
req = make_ref()
|
||||
|
||||
HTTP
|
||||
|
@ -72,12 +73,13 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
|
||||
{:ok, :fake_conn}
|
||||
end)
|
||||
|> expect(:stream_request_body, fn mint, _, _ -> {:ok, mint} end)
|
||||
|> expect(:request, fn mint, _, _, _, _ ->
|
||||
send(self(), :ignore)
|
||||
{:ok, mint, req}
|
||||
end)
|
||||
|> expect(:stream, fn mint, :ignore ->
|
||||
{:ok, mint, [{:done, req}]}
|
||||
{:ok, mint, [{:status, req, 200}, {:done, req}]}
|
||||
end)
|
||||
|
||||
assert conn = Proxy.request(conn, target)
|
||||
|
@ -85,7 +87,7 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
end
|
||||
|
||||
test "it records the outgoing connection", %{conn: conn} do
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
|
||||
ActiveConnections
|
||||
|> expect(:connect, fn ^target -> :ok end)
|
||||
|
@ -103,7 +105,7 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
{:error, %Mint.TransportError{reason: :protocol_not_negotiated}}
|
||||
end)
|
||||
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
assert conn = Proxy.request(conn, target)
|
||||
assert conn.status == 502
|
||||
end
|
||||
|
@ -116,7 +118,7 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
{:error, %Mint.TransportError{reason: :timeout}}
|
||||
end)
|
||||
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
assert conn = Proxy.request(conn, target)
|
||||
assert conn.status == 504
|
||||
end
|
||||
|
@ -141,7 +143,7 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
{:error, :ignore}
|
||||
end)
|
||||
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
Proxy.request(conn, target)
|
||||
end
|
||||
|
||||
|
@ -176,7 +178,7 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
{:error, :ignore}
|
||||
end)
|
||||
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
Proxy.request(conn, target)
|
||||
end
|
||||
|
||||
|
@ -205,7 +207,7 @@ defmodule Wayfarer.Server.ProxyTest do
|
|||
{:error, :ignore}
|
||||
end)
|
||||
|
||||
target = {:http, {127, 0, 0, 1}, random_port()}
|
||||
target = {:http, {127, 0, 0, 1}, random_port(), :auto}
|
||||
Proxy.request(conn, target)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,7 +54,7 @@ defmodule Wayfarer.ServerTest do
|
|||
routing_table: [
|
||||
{
|
||||
{:http, ~i"127.0.0.1", listen_port},
|
||||
{:http, ~i"127.0.0.1", target_port},
|
||||
{:http, ~i"127.0.0.1", target_port, :auto},
|
||||
["example.com"],
|
||||
:round_robin
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ defmodule Wayfarer.ServerTest do
|
|||
|
||||
assert :ets.tab2list(Support.Example) == [
|
||||
{{:http, {127, 0, 0, 1}, listen_port}, {"example", "com"},
|
||||
{:http, {127, 0, 0, 1}, target_port}, :round_robin, :initial}
|
||||
{:http, {127, 0, 0, 1}, target_port, :auto}, :round_robin, :initial}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Wayfarer.Target.SelectorTest do
|
||||
@moduledoc false
|
||||
use ExUnit.Case, async: true
|
||||
use ExUnit.Case, async: false
|
||||
use Plug.Test
|
||||
use Mimic
|
||||
use Support.PortTracker
|
||||
|
|
|
@ -51,9 +51,9 @@ defmodule WayfarerTest do
|
|||
]
|
||||
],
|
||||
routing_table: [
|
||||
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target0_port},
|
||||
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target0_port, :auto},
|
||||
["www.example.com"], :round_robin},
|
||||
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target1_port},
|
||||
{{:http, ~i"127.0.0.1", listener_port}, {:http, ~i"127.0.0.1", target1_port, :auto},
|
||||
["www.example.com"], :round_robin}
|
||||
]
|
||||
)
|
||||
|
@ -69,8 +69,8 @@ defmodule WayfarerTest do
|
|||
|
||||
assert [1, 1] =
|
||||
[
|
||||
{:http, {127, 0, 0, 1}, target0_port},
|
||||
{:http, {127, 0, 0, 1}, target1_port}
|
||||
{:http, {127, 0, 0, 1}, target0_port, :auto},
|
||||
{:http, {127, 0, 0, 1}, target1_port, :auto}
|
||||
]
|
||||
|> TotalConnections.proxy_count()
|
||||
|> Enum.map(&elem(&1, 1))
|
||||
|
@ -85,8 +85,8 @@ defmodule WayfarerTest do
|
|||
|
||||
assert [11, 11] =
|
||||
[
|
||||
{:http, {127, 0, 0, 1}, target0_port},
|
||||
{:http, {127, 0, 0, 1}, target1_port}
|
||||
{:http, {127, 0, 0, 1}, target0_port, :auto},
|
||||
{:http, {127, 0, 0, 1}, target1_port, :auto}
|
||||
]
|
||||
|> TotalConnections.proxy_count()
|
||||
|> Enum.map(&elem(&1, 1))
|
||||
|
|
Loading…
Reference in a new issue