From e2d42e273c93dab015add03de65a52196670637c Mon Sep 17 00:00:00 2001 From: Tristan Sloughter Date: Mon, 7 May 2018 12:17:28 -0600 Subject: [PATCH] version bump to 3.1.0 and additions to docs --- .gitignore | 2 + doc/Makefile | 21 +++ doc/build.sh | 21 +++ doc/github-pandoc.css | 424 ++++++++++++++++++++++++++++++++++++++++++ doc/index.md | 305 ++++++++++++++++++++++++++++++ doc/tpl.html | 86 +++++++++ rebar.config | 1 + src/elli.app.src | 2 +- 8 files changed, 861 insertions(+), 1 deletion(-) create mode 100644 doc/Makefile create mode 100755 doc/build.sh create mode 100644 doc/github-pandoc.css create mode 100644 doc/index.md create mode 100644 doc/tpl.html diff --git a/.gitignore b/.gitignore index ac732da..2013848 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +doc/*.html +!doc/tpl.html _build deps/** ebin/** diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..5d72b03 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,21 @@ +SOURCE_DOCS := $(wildcard *.md) + +EXPORTED_DOCS=\ + $(SOURCE_DOCS:.md=.html) + +RM=/bin/rm + +PANDOC=pandoc + +PANDOC_HTML_OPTIONS=--standalone --highlight-style=tango --template tpl.html -f gfm --to html5 + +%.html : %.md + $(PANDOC) $(PANDOC_HTML_OPTIONS) -o $@ $< + + +.PHONY: all clean + +all : $(EXPORTED_DOCS) + +clean: + - $(RM) -f $(EXPORTED_DOCS) diff --git a/doc/build.sh b/doc/build.sh new file mode 100755 index 0000000..188a5ee --- /dev/null +++ b/doc/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +cd $(dirname $(realpath $0)) + +make clean +make + +# fix internal doc links +sed -i 's/https:\/\/github\.com\/doc\/\([a-zA-Z_-]*\)\.md/\1.html/g' *.html +sed -i 's/\"\([a-zA-Z_-]*\)\.md\([a-zA-Z_-#]*\)\"/\"\1.html\2\"/g' *.html +sed -i 's/\"doc\/\([a-zA-Z_-]*\)\.md\"/\"\1.html\"/g' *.html +sed -i 's/\"\([a-zA-Z_-]*\)\.md\"/\"\1.html\"/g' *.html + +# fix external doc links +sed -i 's/maps\.html\#/http:\/\/erlang.org\/doc\/man\/maps\.html#/g' *.html +sed -i 's/unicode\.html\#/http:\/\/erlang.org\/doc\/man\/unicode\.html#/g' *.html +sed -i 's/maps\.md\#/http:\/\/erlang.org\/doc\/man\/maps\.html#/g' *.html +sed -i 's/unicode\.md\#/http:\/\/erlang.org\/doc\/man\/unicode\.html#/g' *.html + +# cleans up the indentation of code blocks +sed -i 's/ *:first-child{margin-top:0!important}body>*:last-child{margin-bottom:0!important}@media screen{body{box-shadow:0 0 0 1px #cacaca,0 0 0 4px #eee}}h1,h2,h3,h4,h5,h6{margin:20px 0 10px;padding:0;font-weight:bold;-webkit-font-smoothing:subpixel-antialiased;cursor:text}h1{font-size:28px;color:#000}h2{font-size:24px;border-bottom:1px solid #ccc;color:#000}h3{font-size:18px;color:#333}h4{font-size:16px;color:#333}h5{font-size:14px;color:#333}h6{color:#777;font-size:14px}p,blockquote,table,pre{margin:15px 0}ul{padding-left:30px}ol{padding-left:30px}ol li ul:first-of-type{margin-top:0}hr{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC) repeat-x 0 0;border:0 none;color:#ccc;height:4px;padding:0}body>h2:first-child{margin-top:0;padding-top:0}body>h1:first-child{margin-top:0;padding-top:0}body>h1:first-child+h2{margin-top:0;padding-top:0}body>h3:first-child,body>h4:first-child,body>h5:first-child,body>h6:first-child{margin-top:0;padding-top:0}a:first-child h1,a:first-child h2,a:first-child h3,a:first-child h4,a:first-child h5,a:first-child h6{margin-top:0;padding-top:0}h1+p,h2+p,h3+p,h4+p,h5+p,h6+p,ul li>:first-child,ol li>:first-child{margin-top:0}dl{padding:0}dl dt{font-size:14px;font-weight:bold;font-style:italic;padding:0;margin:15px 0 5px}dl dt:first-child{padding:0}dl dt>:first-child{margin-top:0}dl dt>:last-child{margin-bottom:0}dl dd{margin:0 0 15px;padding:0 15px}dl dd>:first-child{margin-top:0}dl dd>:last-child{margin-bottom:0}blockquote{border-left:4px solid #DDD;padding:0 15px;color:#777}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}table{border-collapse:collapse;border-spacing:0;font-size:100%;font:inherit}table th{font-weight:bold;border:1px solid #ccc;padding:6px 13px}table td{border:1px solid #ccc;padding:6px 13px}table tr{border-top:1px solid #ccc;background-color:#fff}table tr:nth-child(2n){background-color:#f8f8f8}img{max-width:100%}code,tt{margin:0 2px;padding:0 5px;white-space:nowrap;border:1px solid #eaeaea;background-color:#f8f8f8;border-radius:3px;font-family:Consolas,'Liberation Mono',Courier,monospace;font-size:12px;color:#333}pre>code{margin:0;padding:0;white-space:pre;border:0;background:transparent}.highlight pre{background-color:#f8f8f8;border:1px solid #ccc;font-size:13px;line-height:19px;overflow:auto;padding:6px 10px;border-radius:3px}pre{background-color:#f8f8f8;border:1px solid #ccc;font-size:13px;line-height:19px;overflow:auto;padding:6px 10px;border-radius:3px}pre code,pre tt{background-color:transparent;border:0}.poetry pre{font-family:Georgia,Garamond,serif!important;font-style:italic;font-size:110%!important;line-height:1.6em;display:block;margin-left:1em}.poetry pre code{font-family:Georgia,Garamond,serif!important;word-break:break-all;word-break:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;hyphens:auto;white-space:pre-wrap}sup,sub,a.footnote{font-size:1.4ex;height:0;line-height:1;vertical-align:super;position:relative}sub{vertical-align:sub;top:-1px}@media print{body{background:#fff}img,pre,blockquote,table,figure{page-break-inside:avoid}body{background:#fff;border:0}code{background-color:#fff;color:#333!important;padding:0 .2em;border:1px solid #dedede}pre{background:#fff}pre code{background-color:white!important;overflow:visible}}@media screen{body.inverted{color:#eee!important;border-color:#555;box-shadow:none}.inverted body,.inverted hr .inverted p,.inverted td,.inverted li,.inverted h1,.inverted h2,.inverted h3,.inverted h4,.inverted h5,.inverted h6,.inverted th,.inverted .math,.inverted caption,.inverted dd,.inverted dt,.inverted blockquote{color:#eee!important;border-color:#555;box-shadow:none}.inverted td,.inverted th{background:#333}.inverted h2{border-color:#555}.inverted hr{border-color:#777;border-width:1px!important}::selection{background:rgba(157,193,200,0.5)}h1::selection{background-color:rgba(45,156,208,0.3)}h2::selection{background-color:rgba(90,182,224,0.3)}h3::selection,h4::selection,h5::selection,h6::selection,li::selection,ol::selection{background-color:rgba(133,201,232,0.3)}code::selection{background-color:rgba(0,0,0,0.7);color:#eee}code span::selection{background-color:rgba(0,0,0,0.7)!important;color:#eee!important}a::selection{background-color:rgba(255,230,102,0.2)}.inverted a::selection{background-color:rgba(255,230,102,0.6)}td::selection,th::selection,caption::selection{background-color:rgba(180,237,95,0.5)}.inverted{background:#0b2531;background:#252a2a}.inverted body{background:#252a2a}.inverted a{color:#acd1d5}}.highlight .c{color:#998;font-style:italic}.highlight .err{color:#a61717;background-color:#e3d2d2}.highlight .k,.highlight .o{font-weight:bold}.highlight .cm{color:#998;font-style:italic}.highlight .cp{color:#999;font-weight:bold}.highlight .c1{color:#998;font-style:italic}.highlight .cs{color:#999;font-weight:bold;font-style:italic}.highlight .gd{color:#000;background-color:#fdd}.highlight .gd .x{color:#000;background-color:#faa}.highlight .ge{font-style:italic}.highlight .gr{color:#a00}.highlight .gh{color:#999}.highlight .gi{color:#000;background-color:#dfd}.highlight .gi .x{color:#000;background-color:#afa}.highlight .go{color:#888}.highlight .gp{color:#555}.highlight .gs{font-weight:bold}.highlight .gu{color:#800080;font-weight:bold}.highlight .gt{color:#a00}.highlight .kc,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr{font-weight:bold}.highlight .kt{color:#458;font-weight:bold}.highlight .m{color:#099}.highlight .s{color:#d14}.highlight .na{color:#008080}.highlight .nb{color:#0086b3}.highlight .nc{color:#458;font-weight:bold}.highlight .no{color:#008080}.highlight .ni{color:#800080}.highlight .ne,.highlight .nf{color:#900;font-weight:bold}.highlight .nn{color:#555}.highlight .nt{color:#000080}.highlight .nv{color:#008080}.highlight .ow{font-weight:bold}.highlight .w{color:#bbb}.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:#099}.highlight .sb,.highlight .sc,.highlight .sd,.highlight .s2,.highlight .se,.highlight .sh,.highlight .si,.highlight .sx{color:#d14}.highlight .sr{color:#009926}.highlight .s1{color:#d14}.highlight .ss{color:#990073}.highlight .bp{color:#999}.highlight .vc,.highlight .vg,.highlight .vi{color:#008080}.highlight .il{color:#099}.highlight .gc{color:#999;background-color:#eaf2f5}.type-csharp .highlight .k,.type-csharp .highlight .kt{color:#00F}.type-csharp .highlight .nf{color:#000;font-weight:normal}.type-csharp .highlight .nc{color:#2b91af}.type-csharp .highlight .nn{color:#000}.type-csharp .highlight .s,.type-csharp .highlight .sc{color:#a31515} diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..2f1d9e6 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,305 @@ +# elli - Erlang web server for HTTP APIs + +[![Hex.pm][hex badge]][hex package] +[![Documentation][doc badge]][docs] +[![Erlang][erlang badge]][erlang downloads] +[![Travis CI][travis badge]][travis builds] +[![Coverage Status][coveralls badge]][coveralls link] +[![MIT License][license badge]](LICENSE) + +[travis builds]: https://travis-ci.org/elli-lib/elli +[travis badge]: https://travis-ci.org/elli-lib/elli.svg +[hex badge]: https://img.shields.io/hexpm/v/elli.svg +[hex package]: https://hex.pm/packages/elli +[latest release]: https://github.com/elli-lib/elli/releases/latest +[erlang badge]: https://img.shields.io/badge/erlang-%E2%89%A518.0-red.svg +[erlang downloads]: http://www.erlang.org/downloads +[doc badge]: https://img.shields.io/badge/docs-edown-green.svg +[docs]: doc/README.md +[coveralls badge]: https://coveralls.io/repos/github/elli-lib/elli/badge.svg?branch=develop +[coveralls link]: https://coveralls.io/github/elli-lib/elli?branch=develop +[license badge]: https://img.shields.io/badge/license-MIT-blue.svg + +Elli is a webserver you can run inside your Erlang application to +expose an HTTP API. Elli is a aimed exclusively at building +high-throughput, low-latency HTTP APIs. If robustness and performance +is more important than general purpose features, then `elli` might be +for you. If you find yourself digging into the implementation of a +webserver, `elli` might be for you. If you're building web services, +not web sites, then `elli` might be for you. + +Elli is used in production at Wooga and Game Analytics. Elli requires +OTP 18.0 or newer. + + +## Installation + +To use `elli` you will need a working installation of Erlang 18.0 (or later). + +Add `elli` to your application by adding it as a dependency to your +[`rebar.config`](http://www.rebar3.org/docs/configuration): + +```erlang +{deps, [elli]}. +``` + +Afterwards you can run: + +```sh +$ rebar3 compile +``` + + +## Usage +```sh +$ rebar3 shell +``` + +```erlang +%% starting elli +1> {ok, Pid} = elli:start_link([{callback, elli_example_callback}, {port, 3000}]). +``` + +## Examples + +### Callback Module + +The best source to learn how to write a callback module +is [src/elli_example_callback.erl](src/elli_example_callback.erl) and +its [generated documentation](doc/elli_example_callback.md). There are a bunch +of examples used in the tests as well as descriptions of all the events. + +A minimal callback module could look like this: + +```erlang +-module(elli_minimal_callback). +-export([handle/2, handle_event/3]). + +-include_lib("elli/include/elli.hrl"). +-behaviour(elli_handler). + +handle(Req, _Args) -> + %% Delegate to our handler function + handle(Req#req.method, elli_request:path(Req), Req). + +handle('GET',[<<"hello">>, <<"world">>], _Req) -> + %% Reply with a normal response. `ok' can be used instead of `200' + %% to signal success. + {ok, [], <<"Hello World!">>}; + +handle(_, _, _Req) -> + {404, [], <<"Not Found">>}. + +%% @doc Handle request events, like request completed, exception +%% thrown, client timeout, etc. Must return `ok'. +handle_event(_Event, _Data, _Args) -> + ok. +``` + + +### Supervisor Childspec + +To add `elli` to a supervisor you can use the following example and adapt it to +your needs. + +```erlang +-module(fancyapi_sup). +-behaviour(supervisor). +-export([start_link/0]). +-export([init/1]). + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +init([]) -> + ElliOpts = [{callback, fancyapi_callback}, {port, 3000}], + ElliSpec = { + fancy_http, + {elli, start_link, [ElliOpts]}, + permanent, + 5000, + worker, + [elli]}, + + {ok, { {one_for_one, 5, 10}, [ElliSpec]} }. +``` + + +## Features + +Here's the features Elli _does_ have: + +* [Rack][]-style request-response. Your handler function gets a + complete request and returns a complete response. There's no + messaging, no receiving data directly from the socket, no writing + responses directly to the socket. It's a very simple and + straightforward API. Have a look at [`elli_example_callback`](elli_example_callback.md) +for examples. + +* Middlewares allow you to add useful features like compression, +encoding, stats, but only have it used when needed. No features you +don't use on the critical path. + +* Short-circuiting of responses using exceptions, allows you to use + "assertions" that return for example 403 permission + denied. `is_allowed(Req) orelse throw({403, [], <<"Permission + denied">>})`. + +* Every client connection gets its own process, isolating the failure +of a request from another. For the duration of the connection, only +one process is involved, resulting in very robust and efficient +code. + +* Binaries everywhere for strings. + +* Instrumentation inside the core of the webserver, triggering user + callbacks. For example when a request completes, the user callback + gets the `request_complete` event which contains timings of all the +different parts of handling a request. There's also events for +clients unexpectedly closing a connection, crashes in the user +callback, etc. + +* Keep alive, using one Erlang process per connection only active +when there is a request from the client. Number of connections is +only limited by RAM and CPU. + +* Chunked transfer in responses for real-time push to clients + +* Basic pipelining. HTTP verbs that does not have side-effects(`GET` + and `HEAD`) can be pipelined, ie. a client supporting pipelining +can send multiple requests down the line and expect the responses +to appear in the same order as requests. Elli processes the +requests one at a time in order, future work could make it possible +to process them in parallel. + +* SSL using built-in Erlang/OTP ssl, nice for low volume admin +interfaces, etc. For high volume, you should probably go with +nginx, stunnel or ELB if you're on AWS. + +* Implement your own connection handling, for WebSockets, streaming + uploads, etc. See [`elli_example_callback_handover`](elli_example_callback_handover.md). + +## Extensions + +* [elli_access_log](https://github.com/elli-lib/elli_access_log): +Access log +* [elli_basicauth](https://github.com/elli-lib/elli_basicauth): +Basic auth +* [elli_chatterbox](https://github.com/elli-lib/elli_chatterbox): +HTTP/2 support +* [elli_cloudfront](https://github.com/elli-lib/elli_cloudfront): +CloudFront signed URLs +* [elli_cookie](https://github.com/elli-lib/elli_cookie): +Cookies +* [elli_date](https://github.com/elli-lib/elli_date): +"Date" header +* [elli_fileserve](https://github.com/elli-lib/elli_fileserve): +Static content +* [elli_prometheus](https://github.com/elli-lib/elli_prometheus): +Prometheus +* [elli_stats](https://github.com/elli-lib/elli_stats): +Real-time statistics dashboard +* [elli_websockets](https://github.com/elli-lib/elli_websocket): +WebSockets +* [elli_xpblfe](https://github.com/elli-lib/elli_xpblfe): +X-Powered-By LFE + +## About + +From operating and debugging high-volume, low-latency apps we have +gained some valuable insight into what we want from a webserver. We +want simplicity, robustness, performance, ease of debugging, +visibility into strange client behaviour, really good instrumentation +and good tests. We are willing to sacrifice almost everything, even +basic features to achieve this. + +With this in mind we looked at the big names in the Erlang +community: [Yaws][], [Mochiweb][], [Misultin][] and [Cowboy][]. We +found [Mochiweb][] to be the best match. However, we also wanted to +see if we could take the architecture of [Mochiweb][] and improve on +it. `elli` takes the acceptor-turns-into-request-handler idea found +in [Mochiweb][], the binaries-only idea from [Cowboy][] and the +request-response idea from [WSGI][]/[Rack][] (with chunked transfer +being an exception). + +On top of this we built a handler that allows us to write HTTP +middleware modules to add practical features, like compression of +responses, HTTP access log with timings, a real-time statistics +dashboard and chaining multiple request handlers. + +## Aren't there enough webservers in the Erlang community already? + +There are a few very mature and robust projects with steady +development, one recently ceased development and one new kid on the +block with lots of interest. As `elli` is not a general purpose +webserver, but more of a specialized tool, we believe it has a very +different target audience and would not attract effort or users away +from the big names. + +## Why another webserver? Isn't this just the NIH syndrome? + +[Yaws][], [Mochiweb][], [Misultin][], and [Cowboy][] are great +projects, hardened over time and full of very useful features for web +development. If you value developer productivity, [Yaws][] is an +excellent choice. If you want a fast and lightweight +server, [Mochiweb][] and [Cowboy][] are excellent choices. + +Having used and studied all of these projects, we believed that if we +merged some of the existing ideas and added some ideas from other +communities, we could create a core that was better for our use cases. + +It started out as an experiment to see if it is at all possible to +significantly improve and it turns out that for our particular use +cases, there is enough improvement to warrant a new project. + +## What makes Elli different? + +Elli has a very simple architecture. It avoids using more processes +and messages than absolutely necessary. It uses binaries for +strings. The request-response programming model allows middlewares to +do much heavy lifting, so the core can stay very simple. It has been +instrumented so as a user you can understand where time is spent. When +things go wrong, like the client closed the connection before you +could send a response, you are notified about these things so you can +better understand your client behaviour. + +## Performance + +"Hello World!" micro-benchmarks are really useful when measuring the +performance of the webserver itself, but the numbers usually do more +harm than good when released. I encourage you to run your own +benchmarks, on your own hardware. Mark Nottingham has some +[very good pointers](http://www.mnot.net/blog/2011/05/18/http_benchmark_rules) +about benchmarking HTTP servers. + +[Yaws]: https://github.com/klacke/yaws +[Mochiweb]: https://github.com/mochi/mochiweb +[Misultin]: https://github.com/ostinelli/misultin +[Cowboy]: https://github.com/ninenines/cowboy +[WSGI]: https://www.python.org/dev/peps/pep-3333/ +[Rack]: https://github.com/rack/rack + + +## Modules ## + + + + + + + + + + + + + + +
elli
elli_example_callback
elli_example_callback_handover
elli_handler
elli_http
elli_middleware
elli_middleware_compress
elli_request
elli_sendfile
elli_tcp
elli_test
elli_util
+ + +## License + +Elli is licensed under [The MIT License](LICENSE). + +Copyright (c) 2012-2016 Knut Nesheim, 2016-2018 elli-lib team diff --git a/doc/tpl.html b/doc/tpl.html new file mode 100644 index 0000000..506b5f2 --- /dev/null +++ b/doc/tpl.html @@ -0,0 +1,86 @@ + + + + + + + index + + + + + + + $body$ + + diff --git a/rebar.config b/rebar.config index 94ac836..ad12c77 100644 --- a/rebar.config +++ b/rebar.config @@ -6,6 +6,7 @@ {docs, [ {deps, [{edown, "0.8.1"}]}, {edoc_opts, [ + {preprocess, true}, {def, [ {'EXAMPLE_CONF',"[{callback,elli_example_callback},{callback_args,[]}]"} ]}, diff --git a/src/elli.app.src b/src/elli.app.src index 5a160dc..9c1f243 100644 --- a/src/elli.app.src +++ b/src/elli.app.src @@ -1,7 +1,7 @@ {application, elli, [ {description, "Erlang web server for HTTP APIs"}, - {vsn, "3.0.0"}, + {vsn, "3.1.0"}, {modules, [ elli, elli_example_callback,