From 0ae2d43d35780c1c214b283e91353663d2afa61b Mon Sep 17 00:00:00 2001 From: Marchandise Rudy Date: Wed, 20 Jan 2021 18:16:29 +0100 Subject: [PATCH] Logging providers & Documentation (#61) * Logs with Seq * Update documentation --- .dockerignore | 3 + Dockerfile.local | 15 ++ README.md | 172 ++++++++++++------ docker-compose.yml | 28 +++ docs/Gemfile | 4 +- docs/Gemfile.lock | 5 + docs/_config.yml | 15 +- docs/assets/images/seq.png | Bin 0 -> 36000 bytes docs/docker-compose.yml | 3 +- docs/examples/echo.kube.yaml | 23 ++- docs/favicon.ico | Bin 0 -> 2462 bytes docs/index.md | 37 +++- docs/pages/configuration/commands.md | 23 +++ .../feature-toggle.md} | 16 +- docs/pages/configuration/index.md | 12 ++ docs/pages/configuration/loggers.md | 36 ++++ docs/pages/docker-compose.md | 47 ----- docs/pages/docker.md | 46 ----- docs/pages/helm.md | 66 ------- docs/pages/includes/section-tests.md | 159 ---------------- docs/pages/kubernetes.md | 93 ---------- docs/pages/quick-start/docker-compose.md | 76 ++++++++ docs/pages/quick-start/docker.md | 72 ++++++++ docs/pages/quick-start/helm.md | 86 +++++++++ .../quick-start/includes/section-examples.md | 144 +++++++++++++++ docs/pages/quick-start/index.md | 6 + docs/pages/quick-start/kubernetes.md | 49 +++++ docs/pages/quick-start/nodejs.md | 70 +++++++ docs/release-notes.md | 20 ++ package-lock.json | 145 +++++++++++++-- package.json | 2 + src/app.js | 6 +- src/global.json | 8 + src/middlewares/logMiddleware.js | 31 +++- test/body.form.js | 12 +- test/body.json.js | 12 +- test/body.text.js | 12 +- test/cookie.js | 22 ++- test/custom.body.js | 10 +- test/custom.code.js | 34 ++-- test/custom.environment.js | 6 +- test/custom.headers.js | 16 +- test/custom.js | 10 +- test/custom.time.js | 14 +- test/environment.js | 4 +- test/file.js | 2 + test/headers.js | 12 +- test/query.js | 12 +- test/verbs.js | 42 +++-- 49 files changed, 1127 insertions(+), 611 deletions(-) create mode 100644 Dockerfile.local create mode 100644 docker-compose.yml create mode 100755 docs/assets/images/seq.png create mode 100755 docs/favicon.ico create mode 100644 docs/pages/configuration/commands.md rename docs/pages/{includes/section-configuration.md => configuration/feature-toggle.md} (76%) create mode 100755 docs/pages/configuration/index.md create mode 100644 docs/pages/configuration/loggers.md delete mode 100644 docs/pages/docker-compose.md delete mode 100644 docs/pages/docker.md delete mode 100644 docs/pages/helm.md delete mode 100644 docs/pages/includes/section-tests.md delete mode 100644 docs/pages/kubernetes.md create mode 100644 docs/pages/quick-start/docker-compose.md create mode 100644 docs/pages/quick-start/docker.md create mode 100644 docs/pages/quick-start/helm.md create mode 100644 docs/pages/quick-start/includes/section-examples.md create mode 100644 docs/pages/quick-start/index.md create mode 100644 docs/pages/quick-start/kubernetes.md create mode 100755 docs/pages/quick-start/nodejs.md create mode 100755 docs/release-notes.md diff --git a/.dockerignore b/.dockerignore index 2dafe74..5cb0969 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,9 @@ .gitignore docs *.md +Dockerfile +Dockerfile.local +docker-compose.yml # Logs logs diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..6240ab7 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,15 @@ +FROM node:lts-alpine3.9 + +WORKDIR /build +COPY package.json . +COPY package-lock.json . +RUN npm install + +COPY . . +RUN npm run build + +WORKDIR /app +RUN cp /build/src/global.json . +RUN cp /build/dist/webserver.js . + +ENTRYPOINT [ "node", "webserver" ] \ No newline at end of file diff --git a/README.md b/README.md index 78a2924..26700d6 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,32 @@ # Echo-Server / Docker / Kubernetes / Helm [![Codecov](https://img.shields.io/codecov/c/github/ealenn/echo-server?style=for-the-badge&logo=codecov)](https://codecov.io/gh/Ealenn/Echo-Server) +[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/ealenn/echo-server?style=for-the-badge)](https://www.codefactor.io/repository/github/ealenn/echo-server) [![GitHub stars](https://img.shields.io/github/stars/Ealenn/Echo-Server?style=for-the-badge&logo=github)](https://github.com/Ealenn/Echo-Server/stargazers) [![GitHub issues](https://img.shields.io/github/issues/Ealenn/Echo-Server?style=for-the-badge&logo=github)](https://github.com/Ealenn/Echo-Server/issues) [![DockerHub](https://img.shields.io/docker/pulls/ealen/echo-server.svg?style=for-the-badge&logo=docker)](https://hub.docker.com/repository/docker/ealen/echo-server) [![DockerHub](https://img.shields.io/badge/SIZE-%3C%2030%20MB-1488C6?style=for-the-badge&logo=docker)](https://hub.docker.com/repository/docker/ealen/echo-server) -> Read the docs : [https://ealenn.github.io/Echo-Server](https://ealenn.github.io/Echo-Server) - Read the [release notes](https://github.com/Ealenn/Echo-Server/releases) +> Read the docs : [https://ealenn.github.io/Echo-Server](https://ealenn.github.io/Echo-Server) An echo server is a server that replicates the request sent by the client and sends it back. Available: +![](https://img.shields.io/badge/linux-amd64-blue?style=flat-square&logo=docker) +![](https://img.shields.io/badge/linux-arm/v6-blue?style=flat-square&logo=docker) +![](https://img.shields.io/badge/linux-arm/v7-blue?style=flat-square&logo=docker) +![](https://img.shields.io/badge/linux-arm64/v8-blue?style=flat-square&logo=docker) +![](https://img.shields.io/badge/linux-ppc64le-blue?style=flat-square&logo=docker) +![](https://img.shields.io/badge/linux-s390x-blue?style=flat-square&logo=docker) + - GET / POST / PUT / PATCH / DELETE - Request (Query, Body, IPs, Host, Urls...) - Request Headers / Response Headers - Environment variables - Control via Headers/Query - Folders and Files - -Docker OS/ARCH : - -- linux/amd64 -- linux/arm/v6 -- linux/arm/v7 -- linux/arm64/v8 -- linux/ppc64le -- linux/s390x +- Monitoring ![docker](https://ealenn.github.io/Echo-Server/assets/images/docker.png) @@ -45,18 +45,20 @@ Docker OS/ARCH : * [File/Folder explorer](#FileFolderexplorer) * [Combine custom actions](#Combinecustomactions) * [Change default Queries/Request commands](#ChangedefaultQueriesRequestcommands) +* [Loggers](#Loggers) + * [Seq](#Seq) * [Setting up](#Settingup) * [Docker](#Docker) * [Docker-Compose](#Docker-Compose) * [Kubernetes](#Kubernetes) * [Kubernetes with Helm](#KuberneteswithHelm) + * [NodeJS](#NodeJS) * [Contributing](#Contributing) * [Versioning](#Versioning) * [License](#License) -* [Local development](#Localdevelopment) - * [Run Echo-Server](#RunEcho-Server) - * [Run documentation server](#Rundocumentationserver) - * [Run tests](#Runtests) +* [Development](#Development) + * [Documentation](#Documentation) + * [Tests](#Tests) * [Release notes](#Releasenotes) * [Update Helm Chart](#UpdateHelmChart) @@ -99,18 +101,16 @@ I use [jq](https://stedolan.github.io/jq) for nice `curl` results ;) #### Custom HTTP Status Code -ECHO_HOST = `localhost:3000` or `echoserver.cluster.local` for Kubernetes by default. - ```bash -➜ curl -I --header 'X-ECHO-CODE: 404' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_code=404 +➜ curl -I --header 'X-ECHO-CODE: 404' localhost:8080 +➜ curl -I localhost:8080/?echo_code=404 HTTP/1.1 404 Not Found ``` ```bash -➜ curl -I --header 'X-ECHO-CODE: 404-300' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_code=404-300 +➜ curl -I --header 'X-ECHO-CODE: 404-300' localhost:8080 +➜ curl -I localhost:8080/?echo_code=404-300 HTTP/1.1 404 Not Found HTTP/1.1 300 Multiple Choices @@ -119,7 +119,7 @@ HTTP/1.1 300 Multiple Choices ```bash ➜ for i in {1..10} ➜ do -➜ curl -I $ECHO_HOST/?echo_code=200-400-500 +➜ curl -I localhost:8080/?echo_code=200-400-500 ➜ done HTTP/1.1 500 Internal Server Error @@ -133,8 +133,8 @@ HTTP/1.1 500 Internal Server Error #### Custom Body ```bash -➜ curl --header 'X-ECHO-BODY: amazing' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_body=amazing +➜ curl --header 'X-ECHO-BODY: amazing' localhost:8080 +➜ curl localhost:8080/?echo_body=amazing "amazing" ``` @@ -142,8 +142,8 @@ HTTP/1.1 500 Internal Server Error #### Custom Body with Environment variable value ```bash -➜ curl --header 'X-ECHO-ENV-BODY: HOSTNAME' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_env_body=HOSTNAME +➜ curl --header 'X-ECHO-ENV-BODY: HOSTNAME' localhost:8080 +➜ curl localhost:8080/?echo_env_body=HOSTNAME "c53a9ed79fa2" ``` @@ -151,7 +151,7 @@ HTTP/1.1 500 Internal Server Error ```bash ➜ for i in {1..10} ➜ do -➜ curl $ECHO_HOST/?echo_env_body=HOSTNAME +➜ curl localhost:8080/?echo_env_body=HOSTNAME ➜ done "c53a9ed79fa2" @@ -164,16 +164,16 @@ HTTP/1.1 500 Internal Server Error #### Custom Headers ```bash -➜ curl --header 'X-ECHO-HEADER: One:1' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_header=One:1 +➜ curl --header 'X-ECHO-HEADER: One:1' localhost:8080 +➜ curl localhost:8080/?echo_header=One:1 HTTP/1.1 200 OK One: 1 ``` ```bash -➜ curl --header 'X-ECHO-HEADER: One:1, Two:2' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_header=One:1,%20Two:2" +➜ curl --header 'X-ECHO-HEADER: One:1, Two:2' localhost:8080 +➜ curl "localhost:8080/?echo_header=One:1,%20Two:2" HTTP/1.1 200 OK One: 1 @@ -183,8 +183,8 @@ Two: 2 #### Custom response latency ```bash -➜ curl --header 'X-ECHO-TIME: 5000' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_time=5000" +➜ curl --header 'X-ECHO-TIME: 5000' localhost:8080 +➜ curl "localhost:8080/?echo_time=5000" ⏳... 5000 ms ``` @@ -201,8 +201,8 @@ You can change default validations with #### File/Folder explorer ```bash -➜ curl --header 'X-ECHO-FILE: /' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_file=/" +➜ curl --header 'X-ECHO-FILE: /' localhost:8080 +➜ curl "localhost:8080/?echo_file=/" ["app", "bin", "etc", "usr", "var"] ``` @@ -210,8 +210,8 @@ You can change default validations with #### Combine custom actions ```bash -➜ curl --header 'X-ECHO-CODE: 401' --header 'X-ECHO-BODY: Oups' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_body=Oups&echo_code=401" +➜ curl --header 'X-ECHO-CODE: 401' --header 'X-ECHO-BODY: Oups' localhost:8080 +➜ curl "localhost:8080/?echo_body=Oups&echo_code=401" HTTP/1.1 401 Unauthorized "Oups" @@ -219,6 +219,8 @@ HTTP/1.1 401 Unauthorized ## Change default Queries/Request commands +[Read the docs](https://ealenn.github.io/Echo-Server/pages/configuration/commands.html) + | Environment | CLI | Default | |------------------------------------|------------------------------------|--------------------| | COMMANDS__HTTPBODY__QUERY | --commands:httpBody:query | `echo_body` | @@ -234,34 +236,77 @@ HTTP/1.1 401 Unauthorized | COMMANDS__FILE__QUERY | --commands:file:query | `echo_file` | | COMMANDS__FILE__HEADER | --commands:file:header | `x-echo-file` | +## Loggers + +[Read the docs](https://ealenn.github.io/Echo-Server/pages/configuration/loggers.html) + +| Environment | CLI | Default | +|------------------------------------|------------------------------------|--------------------| +| LOGS__APP | --logs:app | `echo-server` | +| LOGS__LEVEL | --logs:level | `debug` | + +### Seq + +![seq](https://ealenn.github.io/Echo-Server/assets/images/seq.png) + +| Environment | CLI | Default | +|------------------------------------|------------------------------------|--------------------| +| LOGS__SEQ__ENABLED | --logs:seq:enabled | `false` | +| LOGS__SEQ__SERVER | --logs:seq:server | ` ` | +| LOGS__SEQ__KEY | --logs:seq:key | ` ` | +| LOGS__SEQ__LEVEL | --logs:seq:level | `info` | + ## Setting up ### Docker -[Read the docs](https://ealenn.github.io/Echo-Server/pages/docker.html) +[Read the docs](https://ealenn.github.io/Echo-Server/pages/quick-start/docker.html) ```bash -docker run -p 3000:80 ealen/echo-server +docker run -p 8080:80 ealen/echo-server ``` ### Docker-Compose -[Read the docs](https://ealenn.github.io/Echo-Server/pages/docker-compose.html) +[Read the docs](https://ealenn.github.io/Echo-Server/pages/quick-start/docker-compose.html) + +**Sample** ```yaml -version: '3' +version: "3" services: echo-server: - image: ealen/echo-server:latest - environment: - - ENABLE__ENVIRONMENT=false + image: ealen/echo-server + ports: + - 8080:80 +``` + +**With Seq** + +```yaml +version: "3" + +services: + echo: + image: ealen/echo-server + environment: + PORT: 80 + LOGS__SEQ__ENABLED: "true" + LOGS__SEQ__SERVER: "http://seq:5341" + ports: + - 8080:80 + + seq: + image: datalust/seq + environment: + ACCEPT_EULA: "Y" ports: - - 3000:80 + - 3010:80 ``` ### Kubernetes -[Read the docs](https://ealenn.github.io/Echo-Server/pages/kubernetes.html) +[Read the docs](https://ealenn.github.io/Echo-Server/pages/quick-start/kubernetes.html) ```bash curl -sL https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.kube.yaml | kubectl apply -f - @@ -269,7 +314,7 @@ curl -sL https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/exampl ### Kubernetes with Helm -[Read the docs](https://ealenn.github.io/Echo-Server/pages/helm.html) - [Helm Hub](https://hub.helm.sh/charts/ealenn/echo-server) +[Read the docs](https://ealenn.github.io/Echo-Server/pages/quick-start/helm.html) - [Helm Hub](https://hub.helm.sh/charts/ealenn/echo-server) ```bash helm repo add ealenn https://ealenn.github.io/charts @@ -277,6 +322,19 @@ helm repo update helm install --set ingress.enable=true --name echoserver ealenn/echo-server ``` +### NodeJS + +[Read the docs](https://ealenn.github.io/Echo-Server/pages/quick-start/nodejs) + +```bash +# Dependencies +npm install +# Run with node +node ./src/webserver --port 8080 +# Run with npm script +PORT=8080 npm run start +``` + --- ## Contributing @@ -290,29 +348,23 @@ For the versions available, see the [tags on this repository](https://github.com ## License -This project is licensed under the GNU Lesser General Public License - see the [LICENSE.md](LICENSE.md) file for details. +This project is licensed under the GNU Lesser General Public License - see the [LICENSE.txt](https://github.com/Ealenn/Echo-Server/blob/master/LICENSE.txt) file for details. --- -## Local development - -### Run Echo-Server +## Development -```bash -npm install -node ./src/webserver --port 3000 -# OR -PORT=3000 npm run start -``` +### Documentation -### Run documentation server +Docker-Compose is available on `./docs` folder. ```bash -cd ./docs -docker compose up +docker compose up -d ``` -### Run tests +The documentation is here [localhost:4000](http://localhost:4000) + +### Tests ```bash npm install diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b585083 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3" + +services: + echo: + build: + context: . + dockerfile: Dockerfile.local + environment: + PORT: 80 + LOGS__SEQ__ENABLED: "true" + LOGS__SEQ__SERVER: "http://seq:5341" + ports: + - 8080:80 + networks: + - backend + + seq: + image: datalust/seq:latest + environment: + ACCEPT_EULA: "Y" + ports: + - 8081:80 + - 5341:5341 + networks: + - backend + +networks: + backend: \ No newline at end of file diff --git a/docs/Gemfile b/docs/Gemfile index f94f65e..36d519b 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -1,3 +1,5 @@ source "https://rubygems.org" -gem 'github-pages', group: :jekyll_plugins \ No newline at end of file +gem 'github-pages', group: :jekyll_plugins +gem 'jekyll-github-metadata' +gem 'jekyll-toc' \ No newline at end of file diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 9374bce..339f111 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -190,6 +190,9 @@ GEM jekyll-seo-tag (~> 2.0) jekyll-titles-from-headings (0.5.3) jekyll (>= 3.3, < 5.0) + jekyll-toc (0.16.1) + jekyll (>= 3.8) + nokogiri (~> 1.10) jekyll-watch (2.2.1) listen (~> 3.0) jemoji (0.12.0) @@ -260,6 +263,8 @@ PLATFORMS DEPENDENCIES github-pages + jekyll-github-metadata + jekyll-toc BUNDLED WITH 2.2.4 diff --git a/docs/_config.yml b/docs/_config.yml index 8f34cb8..1e03805 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,12 +1,19 @@ # https://pmarsceill.github.io/just-the-docs/ remote_theme: pmarsceill/just-the-docs -title: Echo Server +title: "Echo-Server" logo: "/assets/images/ping-pong.png" search_enabled: true heading_anchors: true -footer_content: "Copyright © 2019-2020 Rudy Marchandise." +gh_edit_link: true +gh_edit_link_text: "Edit this page on GitHub." +gh_edit_repository: "https://github.com/Ealenn/Echo-Server" +gh_edit_branch: "master" +gh_edit_source: docs +gh_edit_view_mode: "edit" + +footer_content: "Copyright © 2019-2021 Rudy Marchandise." aux_links: "GitHub": - "//github.com/Ealenn/Echo-Server" @@ -14,3 +21,7 @@ aux_links: - "//hub.docker.com/repository/docker/ealen/echo-server" "Helm Hub": - "//hub.helm.sh/charts/ealenn/echo-server" + +plugins: + - jekyll-github-metadata + - jekyll-toc \ No newline at end of file diff --git a/docs/assets/images/seq.png b/docs/assets/images/seq.png new file mode 100755 index 0000000000000000000000000000000000000000..d0c6a061fa67d346159cc1b39bd149d1f6816009 GIT binary patch literal 36000 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 literal 0 HcmV?d00001 diff --git a/docs/docker-compose.yml b/docs/docker-compose.yml index c746c86..9c52897 100644 --- a/docs/docker-compose.yml +++ b/docs/docker-compose.yml @@ -1,4 +1,5 @@ -version: '3' +version: "3" + services: jekyll: image: jekyll/jekyll diff --git a/docs/examples/echo.kube.yaml b/docs/examples/echo.kube.yaml index bb10caf..48bc515 100644 --- a/docs/examples/echo.kube.yaml +++ b/docs/examples/echo.kube.yaml @@ -3,13 +3,16 @@ kind: Namespace metadata: name: echoserver --- -apiVersion: extensions/v1beta1 +apiVersion: apps/v1 kind: Deployment metadata: name: echoserver namespace: echoserver spec: replicas: 5 + selector: + matchLabels: + app: echoserver template: metadata: labels: @@ -39,7 +42,7 @@ spec: selector: app: echoserver --- -apiVersion: extensions/v1beta1 +apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: echoserver @@ -48,10 +51,12 @@ metadata: kubernetes.io/ingress.class: nginx spec: rules: - - host: echoserver.cluster.local - http: - paths: - - path: /echo - backend: - serviceName: echoserver - servicePort: 80 \ No newline at end of file + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: echoserver + port: + number: 80 \ No newline at end of file diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..9222d89124567c61f541e8f406d8df396ac590b7 GIT binary patch literal 2462 zcmb`HSxgf_7{`YY#Umas8efb&`eaNH1+mH@U?~)1Xe0568b#j-F)?1D0%~ng!-FIm zG2jJSEuct6jYI^Mv#8KQgzfeKx|AYEiSYnFXI7@JYfJmUZvLHbzTNr#zd5=H1>t+u zDujPS(WQk5twac|#lk7H9*gAp3qLEx0TI0b08vR)+|BSp`_yKQ!@2FGE1c%%qAhEx zI(D^-f&8|Qu=x%P8?V_XV@|m?%>|n<^36k&Ird2)Qa83jSg{Q@7WVy$J~JFK7asYn z>o4Cj29mps$3t>@NVYY5@LZJIVT>pk7>~R>z(uLK&LrhnmRvrxBoO~%zcIv~8n>W@u5NZXW} z`fc@W9yhu^TZMDTYiNPYnpW8L$oR(#*Dcv^hoq!YAy*i)Qt9wgUmg$IstyYI!uX=a z4QhQ0BuGsV71aZAaRWlGCzi`6?)l%GFmG?x&bZ`d68LsA(*M-sgM4QmugQial^R{Uz_QeY?Y(he6E0 zX)^SP+1I-};BCDI*lH!uC2T5_ZXYBiwDTTb_G4Nqb+4cvTBCNp+UlMr{5*Vy?sn8c zTkQ!Ci!#OSea(fLb+UV#LptR@Zm&9`#{Vgup&U#vkNV{beUz#1V9OTfKR({)L2pdkJo+&Ws|nGDr`|*N$j-QYzC*(38SN|1QnD-U5-Ef#|{g4KX)KF?O{TpkF6yko_BL!`n&Dy%hF k%a8?y%qST5iGt8MuL{Qb5Pr;`u_` +{% for contributor in site.github.contributors %} +
  • + {{ contributor.login }} +
  • +{% endfor %} + diff --git a/docs/pages/configuration/commands.md b/docs/pages/configuration/commands.md new file mode 100644 index 0000000..4b6f33a --- /dev/null +++ b/docs/pages/configuration/commands.md @@ -0,0 +1,23 @@ +--- +layout: default +title: Commands +parent: Configuration +nav_order: 3 +--- + +## Commands + +| Environment | CLI | Default | +|------------------------------------|------------------------------------|--------------------| +| COMMANDS__HTTPBODY__QUERY | --commands:httpBody:query | `echo_body` | +| COMMANDS__HTTPBODY__HEADER | --commands:httpBody:header | `x-echo-body` | +| COMMANDS__HTTPENVBODY__QUERY | --commands:httpEnvBody:query | `echo_env_body` | +| COMMANDS__HTTPENVBODY__HEADER | --commands:httpEnvBody:header | `x-echo-env-body` | +| COMMANDS__HTTPCODE__QUERY | --commands:httpCode:query | `echo_code` | +| COMMANDS__HTTPCODE__HEADER | --commands:httpCode:header | `x-echo-code` | +| COMMANDS__HTTPHEADERS__QUERY | --commands:httpHeaders:query | `echo_header` | +| COMMANDS__HTTPHEADERS__HEADER | --commands:httpHeaders:header | `x-echo-header` | +| COMMANDS__TIME__QUERY | --commands:time:query | `echo_time` | +| COMMANDS__TIME__HEADER | --commands:time:header | `x-echo-time` | +| COMMANDS__FILE__QUERY | --commands:file:query | `echo_file` | +| COMMANDS__FILE__HEADER | --commands:file:header | `x-echo-file` | diff --git a/docs/pages/includes/section-configuration.md b/docs/pages/configuration/feature-toggle.md similarity index 76% rename from docs/pages/includes/section-configuration.md rename to docs/pages/configuration/feature-toggle.md index 5da6c7f..aed81bb 100644 --- a/docs/pages/includes/section-configuration.md +++ b/docs/pages/configuration/feature-toggle.md @@ -1,14 +1,16 @@ - +--- +layout: default +title: Feature-Toggle +parent: Configuration +nav_order: 1 +--- -## Configuration +# Feature Toggle + +This configuration is used to deactivate some elements in the response. | Environment | Helm | CLI | Default | |------------------------------------|----------------------------------|------------------------------------|---------------| -| PORT | service.port | --port | `80` | -| LOGS__IGNORE__PING | application.logs.ignore.ping | --logs:ignore:ping | `false` | | ENABLE__HOST | application.enable.host | --enable:host | `true` | | ENABLE__HTTP | application.enable.http | --enable:http | `true` | | ENABLE__REQUEST | application.enable.request | --enable:request | `true` | diff --git a/docs/pages/configuration/index.md b/docs/pages/configuration/index.md new file mode 100755 index 0000000..a2123e7 --- /dev/null +++ b/docs/pages/configuration/index.md @@ -0,0 +1,12 @@ +--- +layout: default +title: Configuration +has_children: true +nav_order: 3 +--- + +# Configuration + +| Environment | CLI | Default | +|------------------------------------|------------------------------------|---------------| +| PORT | --port | `80` | diff --git a/docs/pages/configuration/loggers.md b/docs/pages/configuration/loggers.md new file mode 100644 index 0000000..44a0839 --- /dev/null +++ b/docs/pages/configuration/loggers.md @@ -0,0 +1,36 @@ +--- +layout: default +title: Loggers +parent: Configuration +nav_order: 2 +--- + +# Loggers +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Standard Streams + +| Environment | CLI | Default | +|------------------------------------|------------------------------------|--------------------| +| LOGS__IGNORE__PING | --logs:ignore:ping | `false` | +| LOGS__APP | --logs:app | `echo-server` | +| LOGS__LEVEL | --logs:level | `debug` | + +## Seq + +![seq](/assets/images/seq.png) + +| Environment | CLI | Default | +|------------------------------------|------------------------------------|--------------------| +| LOGS__SEQ__ENABLED | --logs:seq:enabled | `false` | +| LOGS__SEQ__SERVER | --logs:seq:server | ` ` | +| LOGS__SEQ__KEY | --logs:seq:key | ` ` | +| LOGS__SEQ__LEVEL | --logs:seq:level | `info` | diff --git a/docs/pages/docker-compose.md b/docs/pages/docker-compose.md deleted file mode 100644 index f8b42fc..0000000 --- a/docs/pages/docker-compose.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -layout: default -title: Docker-Compose -parent: Documentation -nav_order: 3 ---- -# Docker-Compose - -List of available [Docker Tags](https://hub.docker.com/r/ealen/echo-server/tags) - Read the [release notes](https://github.com/Ealenn/Echo-Server/releases) - -## Compose File - -```yaml -version: '3' -services: - echo-server: - image: ealen/echo-server:latest - ports: - - 3000:80 -``` - -### Example - -You can use environment variables - -```yaml -version: '3' -services: - echo-server: - image: ealen/echo-server:latest - environment: - - ENABLE__ENVIRONMENT=false - ports: - - 3000:80 -``` - -## Run Echo Server - -```sh -docker-compose up -d -``` - -{% include_relative includes/section-configuration.md %} - ---- - -{% include_relative includes/section-tests.md %} diff --git a/docs/pages/docker.md b/docs/pages/docker.md deleted file mode 100644 index eabd704..0000000 --- a/docs/pages/docker.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: default -title: Docker -parent: Documentation -nav_order: 2 ---- -# Docker - -![cli docker](../assets/images/docker.gif) - -## Run Echo-Server - -List of available [Docker Tags](https://hub.docker.com/r/ealen/echo-server/tags) - Read the [release notes](https://github.com/Ealenn/Echo-Server/releases) - -```sh -docker run -d \ - -p 3000:80 \ - ealen/echo-server -``` - -You can use environment variables with `-e ENV_NAME=VALUE` or CLI arguments after image's name `ealen/echo-server --arg value` - -### Example - -- **With environment variables:** - -```sh -docker run -d \ - -p 3000:80 \ - -e ENABLE__ENVIRONMENT=false - ealen/echo-server -``` - -- **With CLI arguments:** - -```sh -docker run -d \ - -p 3000:80 \ - ealen/echo-server --enable:environment false -``` - -{% include_relative includes/section-configuration.md %} - ---- - -{% include_relative includes/section-tests.md %} diff --git a/docs/pages/helm.md b/docs/pages/helm.md deleted file mode 100644 index e3bd4fd..0000000 --- a/docs/pages/helm.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -layout: default -title: Helm -parent: Documentation -nav_order: 5 ---- -# Deploy Echo-Server with Helm - -## Adding the Repository - -This chart repository can be added to `helm` via - -```sh -helm repo add ealenn https://ealenn.github.io/charts -helm repo update -``` - -More information on [Helm Hub](https://hub.helm.sh/charts/ealenn/echo-server) - -## Deploy Echo-Server with helm - -```sh -helm upgrade -i ${name} ealenn/echo-server --namespace ${namespace} --force -``` - -> *Example: `helm upgrade -i echoserver ealenn/echo-server --namespace echoserver --force`* - -You can override values with [example.values.yaml](https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.helm.yaml) file - -### *Example* - -```yaml -replicaCount: 5 - -ingress: - enabled: true - hosts: - - host: echo.cluster.local - paths: - - / - annotations: - kubernetes.io/ingress.class: nginx - - application: - logs: - ignore: - ping: true -``` - -```bash -curl https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.helm.yaml --output ./example.values.yaml - -helm upgrade -i -f ./example.values.yaml echoserver ealenn/echo-server --namespace echoserver --force -``` - -You can use **Nginx Ingress Controller** for try Echo-Server with : - -```sh -helm install stable/nginx-ingress --name nginx --namespace nginx -``` - -{% include_relative includes/section-configuration.md %} - ---- - -{% include_relative includes/section-tests.md %} diff --git a/docs/pages/includes/section-tests.md b/docs/pages/includes/section-tests.md deleted file mode 100644 index e872cd3..0000000 --- a/docs/pages/includes/section-tests.md +++ /dev/null @@ -1,159 +0,0 @@ - - -## Use Echo-Server - -- ECHO_HOST = `localhost:3000` or `echoserver.cluster.local` for Kubernetes by default. - -I use [jq](https://stedolan.github.io/jq) for nice `curl` results ;) - -![curl](https://ealenn.github.io/Echo-Server/assets/images/curl.png) - -### Custom responses - -| Query | Header | Content | Conditions | -|---------------------|-----------------------|----------------------------------| ------------------------- | -| ?echo_code= | X-ECHO-CODE | HTTP code `200`, `404` | 200 <= `CODE` <= 599 | -| | | `404-401` or `200-500-301` | | -| ?echo_body= | X-ECHO-BODY | Body message | | -| ?echo_env_body= | X-ECHO-ENV-BODY | The key of environment variable | Enable environment `true` | -| ?echo_header= | X-ECHO-HEADER | Response Header `Lang: en-US` | Enable header `true` | -| ?echo_time= | X-ECHO-TIME | Wait time in `ms` | 0 <= `TIME` <= 30.000 | -| ?echo_file= | X-ECHO-FILE | Path of Directory or File | Enable file `true` | - -#### Custom HTTP Status Code - -```bash -➜ curl -I --header 'X-ECHO-CODE: 404' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_code=404 - -HTTP/1.1 404 Not Found -``` - -```bash -➜ curl -I --header 'X-ECHO-CODE: 404-300' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_code=404-300 - -HTTP/1.1 404 Not Found -HTTP/1.1 300 Multiple Choices -``` - -```bash -➜ for i in {1..10} -➜ do -➜ curl -I $ECHO_HOST/?echo_code=200-400-500 -➜ done - -HTTP/1.1 500 Internal Server Error -HTTP/1.1 400 Bad Request -HTTP/1.1 200 OK -HTTP/1.1 500 Internal Server Error -HTTP/1.1 200 OK -HTTP/1.1 500 Internal Server Error -``` - -#### Custom Body - -```bash -➜ curl --header 'X-ECHO-BODY: amazing' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_body=amazing - -"amazing" -``` - -#### Custom Body with environment variable value - -```bash -➜ curl --header 'X-ECHO-ENV-BODY: HOSTNAME' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_env_body=HOSTNAME - -"c53a9ed79fa2" -``` - -```bash -➜ for i in {1..10} -➜ do -➜ curl $ECHO_HOST/?echo_env_body=HOSTNAME -➜ done - -"c53a9ed79fa2" -"f10c3af61e40" -"c53a9ed79fa2" -"f10c3af61e40" -"c53a9ed79fa2" -``` - -#### Custom Headers - -```bash -➜ curl --header 'X-ECHO-HEADER: One:1' $ECHO_HOST -➜ curl $ECHO_HOST/?echo_header=One:1 - -HTTP/1.1 200 OK -One: 1 -``` - -```bash -➜ curl --header 'X-ECHO-HEADER: One:1, Two:2' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_header=One:1,%20Two:2" - -HTTP/1.1 200 OK -One: 1 -Two: 2 -``` - -#### Custom response latency - -```bash -➜ curl --header 'X-ECHO-TIME: 5000' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_time=5000" - -⏳... 5000 ms -``` - -You can change default validations with - -| ENVIRONMENT | CLI | Default | -|----------------------------|---------------------------| ---------| -| CONTROLS__TIMES__MIN | --controls:times:min | `0` | -| CONTROLS__TIMES__MAX | --controls:times:max | `60000` | - -*(Latency is defined in milliseconds)* - -#### File/Folder explorer - -```bash -➜ curl --header 'X-ECHO-FILE: /' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_file=/" - -["app", "bin", "etc", "usr", "var"] -``` - -#### Combine custom actions - -```bash -➜ curl --header 'X-ECHO-CODE: 401' --header 'X-ECHO-BODY: Oups' $ECHO_HOST -➜ curl "$ECHO_HOST/?echo_body=Oups&echo_code=401" - -HTTP/1.1 401 Unauthorized -"Oups" -``` - -### Change default Queries/Request commands - -| Environment | CLI | Default | -|------------------------------------|------------------------------------|--------------------| -| COMMANDS__HTTPBODY__QUERY | --commands:httpBody:query | `echo_body` | -| COMMANDS__HTTPBODY__HEADER | --commands:httpBody:header | `x-echo-body` | -| COMMANDS__HTTPENVBODY__QUERY | --commands:httpEnvBody:query | `echo_env_body` | -| COMMANDS__HTTPENVBODY__HEADER | --commands:httpEnvBody:header | `x-echo-env-body` | -| COMMANDS__HTTPCODE__QUERY | --commands:httpCode:query | `echo_code` | -| COMMANDS__HTTPCODE__HEADER | --commands:httpCode:header | `x-echo-code` | -| COMMANDS__HTTPHEADERS__QUERY | --commands:httpHeaders:query | `echo_header` | -| COMMANDS__HTTPHEADERS__HEADER | --commands:httpHeaders:header | `x-echo-header` | -| COMMANDS__TIME__QUERY | --commands:time:query | `echo_time` | -| COMMANDS__TIME__HEADER | --commands:time:header | `x-echo-time` | -| COMMANDS__FILE__QUERY | --commands:file:query | `echo_file` | -| COMMANDS__FILE__HEADER | --commands:file:header | `x-echo-file` | diff --git a/docs/pages/kubernetes.md b/docs/pages/kubernetes.md deleted file mode 100644 index 4b9836b..0000000 --- a/docs/pages/kubernetes.md +++ /dev/null @@ -1,93 +0,0 @@ ---- -layout: default -title: Kubernetes -parent: Documentation -nav_order: 4 ---- -# Kubernetes - -## Deploy Echo-Server with Kubectl - -```sh -curl -sL https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.kube.yaml | kubectl apply -f - -``` - -This kube definition: - -- Creates namespace `echoserver` -- Creates `echoserver` deployment with `5` replicas -- Creates Ingress with `kubernetes.io/ingress.class: nginx` annotation - -You can use an **Nginx Ingress Controller**: - -```sh -kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud-generic.yaml -``` - -Or customize directly this deployment: - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: echoserver ---- -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: echoserver - namespace: echoserver -spec: - replicas: 5 - template: - metadata: - labels: - app: echoserver - spec: - containers: - - image: ealen/echo-server:latest - imagePullPolicy: IfNotPresent - name: echoserver - ports: - - containerPort: 80 - env: - - name: PORT - value: "80" ---- -apiVersion: v1 -kind: Service -metadata: - name: echoserver - namespace: echoserver -spec: - ports: - - port: 80 - targetPort: 80 - protocol: TCP - type: ClusterIP - selector: - app: echoserver ---- -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: echoserver - namespace: echoserver - annotations: - kubernetes.io/ingress.class: nginx -spec: - rules: - - host: echoserver.cluster.local - http: - paths: - - path: /echo - backend: - serviceName: echoserver - servicePort: 80 -``` - -{% include_relative includes/section-configuration.md %} - ---- - -{% include_relative includes/section-tests.md %} diff --git a/docs/pages/quick-start/docker-compose.md b/docs/pages/quick-start/docker-compose.md new file mode 100644 index 0000000..01e5f67 --- /dev/null +++ b/docs/pages/quick-start/docker-compose.md @@ -0,0 +1,76 @@ +--- +layout: default +title: Docker-Compose +parent: Quick-Start +nav_order: 3 +--- +# Docker-Compose +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Run + +```yaml +version: "3" +services: + echo-server: + image: ealen/echo-server:{{ site.github.releases[0].tag_name }} + ports: + - 3000:80 +``` + +```sh +➜ docker-compose up -d +➜ curl -I localhost:3000 +HTTP/1.1 200 OK +``` + +## Configuration + +You can use environment variables [More information](/pages/configuration) + +```yaml +version: "3" +services: + echo-server: + image: ealen/echo-server:{{ site.github.releases[0].tag_name }} + environment: + - ENABLE__ENVIRONMENT=false + ports: + - 3000:80 +``` + +## Loggers + +[More information](/pages/configuration/loggers) + +```yaml +version: "3" +services: + echo: + image: ealen/echo-server:{{ site.github.releases[0].tag_name }} + environment: + PORT: 80 + LOGS__SEQ__ENABLED: "true" + LOGS__SEQ__SERVER: "http://seq:5341" + ports: + - 3000:80 + + seq: + image: datalust/seq:{{ site.github.releases[0].tag_name }} + environment: + ACCEPT_EULA: "Y" + ports: + - 3010:80 +``` + +## Examples + +{% include_relative includes/section-examples.md host="localhost:3000" %} \ No newline at end of file diff --git a/docs/pages/quick-start/docker.md b/docs/pages/quick-start/docker.md new file mode 100644 index 0000000..a6745c7 --- /dev/null +++ b/docs/pages/quick-start/docker.md @@ -0,0 +1,72 @@ +--- +layout: default +title: Docker +parent: Quick-Start +nav_order: 2 +--- +# Docker +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +![cli docker](../../assets/images/docker.gif) + +## Run + +```sh +➜ docker run -d \ + -p 3000:80 \ + ealen/echo-server:{{ site.github.releases[0].tag_name }} + +➜ curl -I localhost:3000 +HTTP/1.1 200 OK +``` + +## Configuration + +You can use environment variables with `-e ENV_NAME=VALUE` or CLI arguments after image's name `ealen/echo-server:{{ site.github.releases[0].tag_name }} --arg value` [More information](/pages/configuration) + +- **With environment variables:** + +```sh +➜ docker run -d \ + -p 3000:80 \ + -e ENABLE__ENVIRONMENT=false + ealen/echo-server:{{ site.github.releases[0].tag_name }} +``` + +- **With CLI arguments:** + +```sh +➜ docker run -d \ + -p 3000:80 \ + ealen/echo-server:{{ site.github.releases[0].tag_name }} --enable:environment false +``` + +## Loggers + +[More information](/pages/configuration/loggers) + +```sh +➜ docker run -d \ + -p 3000:80 \ + -e LOGS__SEQ__ENABLED=true \ + -e LOGS__SEQ__SERVER=http://localhost:5341 \ + ealen/echo-server:{{ site.github.releases[0].tag_name }} + +➜ docker run -d \ + -p 3010:3010 \ + -p 5341:5341 \ + -e ACCEPT_EULA=Y \ + datalust/seq:{{ site.github.releases[0].tag_name }} +``` + +## Examples + +{% include_relative includes/section-examples.md host="localhost:3000" %} \ No newline at end of file diff --git a/docs/pages/quick-start/helm.md b/docs/pages/quick-start/helm.md new file mode 100644 index 0000000..7aff82d --- /dev/null +++ b/docs/pages/quick-start/helm.md @@ -0,0 +1,86 @@ +--- +layout: default +title: Helm +parent: Quick-Start +nav_order: 5 +--- +# Helm +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Repository + +This chart repository can be added to `helm` with + +```sh +helm repo add ealenn https://ealenn.github.io/charts +helm repo update +``` + +More information on [Artifact Hub](https://artifacthub.io/packages/helm/ealenn/echo-server) + +## Deploy + +```sh +helm upgrade -i ${name} ealenn/echo-server --namespace ${namespace} --force +``` + +> *Example: `helm upgrade -i echo-server ealenn/echo-server --namespace echo-server --force`* + +### Chart Values + +You can override values with [example.values.yaml](https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.helm.yaml) file + +```bash +curl https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.helm.yaml \ + --output ./example.values.yaml +``` + +{% capture code %} +{% highlight yaml linenos %} +replicaCount: 5 + +ingress: + enabled: true + hosts: + - host: echo.cluster.local + paths: + - / + annotations: + kubernetes.io/ingress.class: nginx + + application: + logs: + ignore: + ping: true +{% endhighlight %} +{% endcapture %} +{% include fix_linenos.html code=code %} +{% assign code = nil %} + +```bash +helm upgrade -i \ + -f ./example.values.yaml \ + echo-server ealenn/echo-server \ + --namespace echo-server \ + --force +``` + +## Ingress Controller + +You can use **Nginx Ingress Controller** for try Echo-Server with : + +```sh +helm install stable/nginx-ingress --name nginx --namespace nginx +``` + +## Examples + +{% include_relative includes/section-examples.md host="echo.cluster.local" %} \ No newline at end of file diff --git a/docs/pages/quick-start/includes/section-examples.md b/docs/pages/quick-start/includes/section-examples.md new file mode 100644 index 0000000..0dc4a01 --- /dev/null +++ b/docs/pages/quick-start/includes/section-examples.md @@ -0,0 +1,144 @@ + + +### Custom responses +{: .no_toc } + +| Query | Header | Content | Conditions | +|---------------------|-----------------------|----------------------------------| ------------------------- | +| ?echo_code= | X-ECHO-CODE | HTTP code `200`, `404` | 200 <= `CODE` <= 599 | +| | | `404-401` or `200-500-301` | | +| ?echo_body= | X-ECHO-BODY | Body message | | +| ?echo_env_body= | X-ECHO-ENV-BODY | The key of environment variable | Enable environment `true` | +| ?echo_header= | X-ECHO-HEADER | Response Header `Lang: en-US` | Enable header `true` | +| ?echo_time= | X-ECHO-TIME | Wait time in `ms` | 0 <= `TIME` <= 60.000 | +| ?echo_file= | X-ECHO-FILE | Path of Directory or File | Enable file `true` | + +You can change [commands](/pages/configuration/commands) and [conditions](/pages/configuration/configuration). + +#### Custom HTTP Status Code +{: .no_toc } + +```bash +➜ curl -I --header 'X-ECHO-CODE: 404' {{ include.host }} +➜ curl -I {{ include.host }}/?echo_code=404 + +HTTP/1.1 404 Not Found +``` + +```bash +➜ curl -I --header 'X-ECHO-CODE: 404-300' {{ include.host }} +➜ curl -I {{ include.host }}/?echo_code=404-300 + +HTTP/1.1 404 Not Found +HTTP/1.1 300 Multiple Choices +``` + +```bash +➜ for i in {1..10} +➜ do +➜ curl -I {{ include.host }}/?echo_code=200-400-500 +➜ done + +HTTP/1.1 500 Internal Server Error +HTTP/1.1 400 Bad Request +HTTP/1.1 200 OK +HTTP/1.1 500 Internal Server Error +HTTP/1.1 200 OK +HTTP/1.1 500 Internal Server Error +``` + +#### Custom Body +{: .no_toc } + +```bash +➜ curl --header 'X-ECHO-BODY: amazing' {{ include.host }} +➜ curl {{ include.host }}/?echo_body=amazing + +"amazing" +``` + +#### Custom Body with environment variable value +{: .no_toc } + +```bash +➜ curl --header 'X-ECHO-ENV-BODY: HOSTNAME' {{ include.host }} +➜ curl {{ include.host }}/?echo_env_body=HOSTNAME + +"c53a9ed79fa2" +``` + +```bash +➜ for i in {1..10} +➜ do +➜ curl {{ include.host }}/?echo_env_body=HOSTNAME +➜ done + +"c53a9ed79fa2" +"f10c3af61e40" +"c53a9ed79fa2" +"f10c3af61e40" +"c53a9ed79fa2" +``` + +#### Custom Headers +{: .no_toc } + +```bash +➜ curl --header 'X-ECHO-HEADER: One:1' {{ include.host }} +➜ curl {{ include.host }}/?echo_header=One:1 + +HTTP/1.1 200 OK +One: 1 +``` + +```bash +➜ curl --header 'X-ECHO-HEADER: One:1, Two:2' {{ include.host }} +➜ curl "{{ include.host }}/?echo_header=One:1,%20Two:2" + +HTTP/1.1 200 OK +One: 1 +Two: 2 +``` + +#### Custom response latency +{: .no_toc } + +```bash +➜ curl --header 'X-ECHO-TIME: 5000' {{ include.host }} +➜ curl "{{ include.host }}/?echo_time=5000" + +⏳... 5000 ms +``` + +You can change default validations with + +| ENVIRONMENT | CLI | Default | +|----------------------------|---------------------------| ---------| +| CONTROLS__TIMES__MIN | --controls:times:min | `0` | +| CONTROLS__TIMES__MAX | --controls:times:max | `60000` | + +*(Latency is defined in milliseconds)* + +#### File/Folder explorer +{: .no_toc } + +```bash +➜ curl --header 'X-ECHO-FILE: /' {{ include.host }} +➜ curl "{{ include.host }}/?echo_file=/" + +["app", "bin", "etc", "usr", "var"] +``` + +#### Combine custom actions +{: .no_toc } + +```bash +➜ curl --header 'X-ECHO-CODE: 401' --header 'X-ECHO-BODY: Oups' {{ include.host }} +➜ curl "{{ include.host }}/?echo_body=Oups&echo_code=401" + +HTTP/1.1 401 Unauthorized +"Oups" +``` diff --git a/docs/pages/quick-start/index.md b/docs/pages/quick-start/index.md new file mode 100644 index 0000000..0e7e621 --- /dev/null +++ b/docs/pages/quick-start/index.md @@ -0,0 +1,6 @@ +--- +layout: default +title: Quick-Start +has_children: true +nav_order: 2 +--- \ No newline at end of file diff --git a/docs/pages/quick-start/kubernetes.md b/docs/pages/quick-start/kubernetes.md new file mode 100644 index 0000000..8185510 --- /dev/null +++ b/docs/pages/quick-start/kubernetes.md @@ -0,0 +1,49 @@ +--- +layout: default +title: Kubernetes +parent: Quick-Start +nav_order: 4 +--- +# Kubernetes +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Deploy + +![](https://img.shields.io/badge/K8S-1.19+-blue?style=flat-square&logo=kubernetes) + + +### Definition + +```sh +curl -sL https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.kube.yaml > echo.kube.yaml +``` + +or customize directly [this definition](https://raw.githubusercontent.com/Ealenn/Echo-Server/master/docs/examples/echo.kube.yaml) + +This kube definition: + +- Creates namespace +- Creates deployment with `5` replicas +- Creates Ingress with `kubernetes.io/ingress.class: nginx` annotation + +### Run + +```sh +kubectl apply -f ./echo.kube.yaml +``` + +## Ingress Controller + +You can use an **Nginx Ingress Controller** [More information](https://kubernetes.github.io/ingress-nginx/deploy/) + +## Examples + +{% include_relative includes/section-examples.md host="echo.cluster.local" %} \ No newline at end of file diff --git a/docs/pages/quick-start/nodejs.md b/docs/pages/quick-start/nodejs.md new file mode 100755 index 0000000..c9ead31 --- /dev/null +++ b/docs/pages/quick-start/nodejs.md @@ -0,0 +1,70 @@ +--- +layout: default +title: NodeJS +parent: Quick-Start +nav_order: 1 +--- +# NodeJS +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Clone repository + +```sh +➜ git clone https://github.com/Ealenn/Echo-Server.git +``` + +Or download source code of latest release [here](https://github.com/Ealenn/Echo-Server/releases/latest) + +And install dependencies + +```sh +➜ npm i +``` + +## Run + +```bash +# Run with node +➜ node ./src/webserver --port 8080 +# Run with npm script +➜ PORT=8080 npm run start +``` + +## Configuration + +You can use environment variables or CLI arguments [More information](/pages/configuration) + +- **With environment variables:** + +```sh +➜ PORT=8080 npm run start +``` + +- **With CLI arguments:** + +```sh +➜ node ./src/webserver --port 8080 --enable:environment false +``` + +## Loggers + +[More information](/pages/configuration/loggers) + +```sh +➜ node ./src/webserver \ + --port 8080 \ + --logs:seq:enabled true \ + --logs:seq:server http://localhost:5341 +``` + +## Examples + +{% include_relative includes/section-examples.md host="localhost:8080" %} \ No newline at end of file diff --git a/docs/release-notes.md b/docs/release-notes.md new file mode 100755 index 0000000..d76f6c1 --- /dev/null +++ b/docs/release-notes.md @@ -0,0 +1,20 @@ +--- +layout: default +title: Release Notes +has_children: false +nav_order: 10 +--- + +# Release Notes + +{% for release in site.github.releases %} +## [{{ release.tag_name }}]({{release.html_url}}) + +{{ release.body }} + +[![](https://img.shields.io/badge/Download-TAR-green?style=flat-square&logo=github)]({{release.tarball_url}}) +[![](https://img.shields.io/badge/Download-ZIP-green?style=flat-square&logo=github)]({{release.zipball_url}}) +[![](https://img.shields.io/badge/View-GitHub-lightgrey?style=flat-square&logo=github)]({{release.html_url}}) +[![](https://img.shields.io/badge/View-Docker-blue?style=flat-square&logo=docker)](https://hub.docker.com/r/ealen/echo-server/tags?page=1&ordering=last_updated&name={{release.tag_name}}) + +{% endfor %} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c4cc73d..4029e09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -646,8 +646,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -765,7 +764,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -899,6 +897,34 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "bunyan": { + "version": "1.8.15", + "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", + "integrity": "sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig==", + "requires": { + "dtrace-provider": "~0.8", + "moment": "^2.19.3", + "mv": "~2", + "safe-json-stringify": "~1" + } + }, + "bunyan-seq": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/bunyan-seq/-/bunyan-seq-0.5.0.tgz", + "integrity": "sha512-tnRFVp27MaU8Di5v7jMM8H6/w70LWkVtTDQsXQztutInjjPBCKV07noQMx7fnPcD7zXPLAd4JdeoadmeryCXOA==", + "requires": { + "commander": "^5.1.0", + "seq-logging": "^0.5.0", + "split2": "^3.1.1" + }, + "dependencies": { + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + } + } + }, "busboy": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", @@ -1163,8 +1189,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -1527,6 +1552,15 @@ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, + "dtrace-provider": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", + "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", + "optional": true, + "requires": { + "nan": "^2.14.0" + } + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -2610,7 +2644,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -3427,7 +3460,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3673,6 +3705,12 @@ } } }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "optional": true + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -3718,11 +3756,45 @@ "xtend": "^4.0.0" } }, + "mv": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "optional": true, + "requires": { + "mkdirp": "~0.5.1", + "ncp": "~2.0.0", + "rimraf": "~2.4.0" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "optional": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "rimraf": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "optional": true, + "requires": { + "glob": "^6.0.1" + } + } + } + }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, "optional": true }, "nanomatch": { @@ -3755,6 +3827,12 @@ "yargs": "^3.19.0" } }, + "ncp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "optional": true + }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -4225,7 +4303,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -4409,8 +4486,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -4821,6 +4897,12 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safe-json-stringify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", + "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "optional": true + }, "safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", @@ -4884,6 +4966,11 @@ } } }, + "seq-logging": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/seq-logging/-/seq-logging-0.5.0.tgz", + "integrity": "sha512-j6rGQTuWTEONEeMVO1Mmjsc5P8r1NVGU/V70VxcKJGDL4n5MpYITcNM99+DHEfwx1D6QizURn+xmFpMIR6ETFA==" + }, "serialize-javascript": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", @@ -5168,6 +5255,39 @@ "extend-shallow": "^3.0.0" } }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -6562,8 +6682,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "3.0.1", diff --git a/package.json b/package.json index 74a1229..1a609ba 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,8 @@ "homepage": "https://github.com/Ealenn/Echo-Server#readme", "dependencies": { "body-parser": "^1.19.0", + "bunyan": "^1.8.15", + "bunyan-seq": "^0.5.0", "cookie-parser": "^1.4.4", "express": "^4.17.1", "multer": "^1.4.2", diff --git a/src/app.js b/src/app.js index df98a15..59ea22f 100644 --- a/src/app.js +++ b/src/app.js @@ -15,11 +15,13 @@ app.use(require('cookie-parser')()); app.use(require('multer')().array()); // Middlewares -app.use(require('./middlewares/logMiddleware')); -app.use(require('./middlewares/showFileMiddleware')); app.use(require('./middlewares/customResponseTime')); app.use(require('./middlewares/customHttpCodeMiddleware')); app.use(require('./middlewares/customHttpHeadersMiddleware')); +app.use(require('./middlewares/logMiddleware')); + +// Change Body +app.use(require('./middlewares/showFileMiddleware')); app.use(require('./middlewares/customHttpEnvBodyMiddleware')); app.use(require('./middlewares/customHttpBodyMiddleware')); diff --git a/src/global.json b/src/global.json index c0d51fd..939ba20 100644 --- a/src/global.json +++ b/src/global.json @@ -1,8 +1,16 @@ { "port": 80, "logs": { + "app": "echo-server", + "level": "debug", "ignore": { "ping": false + }, + "seq": { + "enabled": false, + "server": "", + "key": "", + "level": "info" } }, "enable": { diff --git a/src/middlewares/logMiddleware.js b/src/middlewares/logMiddleware.js index 1a34315..05f19dd 100644 --- a/src/middlewares/logMiddleware.js +++ b/src/middlewares/logMiddleware.js @@ -1,7 +1,36 @@ +const bunyan = require('bunyan'); +const seq = require('bunyan-seq'); const config = require('../nconf'); + +// Streams +let streams = [{ + stream: process.stdout, + level: config.get('logs:level'), +}]; + +if (config.get('logs:seq:enabled')) { + streams.push(seq.createStream({ + name: "seq", + serverUrl: config.get('logs:seq:server'), + level: config.get('logs:seq:level'), + apiKey: config.get('logs:seq:key'), + })); +} + +// Logger +const log = bunyan.createLogger({ + name: config.get('logs:app'), + streams +}); + module.exports = (req, res, next) => { if (req.originalUrl != "/ping" || !config.get('logs:ignore:ping')) { - console.log(`${new Date().toUTCString()} | [${req.method}] - ${req.protocol}://${req.get('host')}${req.originalUrl}`); + log.info({ + host: require('../response/host')(req), + http: require('../response/http')(req), + request: require('../response/request')(req), + environment: require('../response/environment')(req) + }, `${new Date().toUTCString()} | [${req.method}] - ${req.protocol}://${req.get('host')}${req.originalUrl}`); } next(); } \ No newline at end of file diff --git a/test/body.form.js b/test/body.form.js index f24d0e5..becb26a 100644 --- a/test/body.form.js +++ b/test/body.form.js @@ -2,6 +2,8 @@ const assert = require('assert'); const request = require('supertest'); const fieldText = 'test with field'; +process.env.LOGS__LEVEL = "error"; + describe('Body with FORM', function () { var server; beforeEach(function () { @@ -15,7 +17,7 @@ describe('Body with FORM', function () { .get('/') .field('text', fieldText) .expect(function (res) { - assert.equal(res.body.request.body.text, fieldText) + assert.strictEqual(res.body.request.body.text, fieldText) }) .expect(200, done); }); @@ -24,7 +26,7 @@ describe('Body with FORM', function () { .post('/') .field('text', fieldText) .expect(function (res) { - assert.equal(res.body.request.body.text, fieldText) + assert.strictEqual(res.body.request.body.text, fieldText) }) .expect(200, done); }); @@ -33,7 +35,7 @@ describe('Body with FORM', function () { .put('/') .field('text', fieldText) .expect(function (res) { - assert.equal(res.body.request.body.text, fieldText) + assert.strictEqual(res.body.request.body.text, fieldText) }) .expect(200, done); }); @@ -42,7 +44,7 @@ describe('Body with FORM', function () { .patch('/') .field('text', fieldText) .expect(function (res) { - assert.equal(res.body.request.body.text, fieldText) + assert.strictEqual(res.body.request.body.text, fieldText) }) .expect(200, done); }); @@ -51,7 +53,7 @@ describe('Body with FORM', function () { .delete('/') .field('text', fieldText) .expect(function (res) { - assert.equal(res.body.request.body.text, fieldText) + assert.strictEqual(res.body.request.body.text, fieldText) }) .expect(200, done); }); diff --git a/test/body.json.js b/test/body.json.js index 316c47a..6b8cdeb 100644 --- a/test/body.json.js +++ b/test/body.json.js @@ -4,6 +4,8 @@ const bodyText = { test: "with json" }; +process.env.LOGS__LEVEL = "error"; + describe('Body with JSON', function () { var server; beforeEach(function () { @@ -17,7 +19,7 @@ describe('Body with JSON', function () { .get('/') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body.test, bodyText.test) + assert.strictEqual(res.body.request.body.test, bodyText.test) }) .expect(200, done); }); @@ -26,7 +28,7 @@ describe('Body with JSON', function () { .post('/') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body.test, bodyText.test) + assert.strictEqual(res.body.request.body.test, bodyText.test) }) .expect(200, done); }); @@ -35,7 +37,7 @@ describe('Body with JSON', function () { .put('/') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body.test, bodyText.test) + assert.strictEqual(res.body.request.body.test, bodyText.test) }) .expect(200, done); }); @@ -44,7 +46,7 @@ describe('Body with JSON', function () { .patch('/') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body.test, bodyText.test) + assert.strictEqual(res.body.request.body.test, bodyText.test) }) .expect(200, done); }); @@ -53,7 +55,7 @@ describe('Body with JSON', function () { .delete('/') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body.test, bodyText.test) + assert.strictEqual(res.body.request.body.test, bodyText.test) }) .expect(200, done); }); diff --git a/test/body.text.js b/test/body.text.js index 39158bf..d37c603 100644 --- a/test/body.text.js +++ b/test/body.text.js @@ -2,6 +2,8 @@ const assert = require('assert'); const request = require('supertest'); const bodyText = 'test with body'; +process.env.LOGS__LEVEL = "error"; + describe('Body with TEXT', function () { var server; beforeEach(function () { @@ -17,7 +19,7 @@ describe('Body with TEXT', function () { .send(bodyText) .expect(function (res) { console.log(res.body.request.body) - assert.equal(res.body.request.body, bodyText) + assert.strictEqual(res.body.request.body, bodyText) }) .expect(200, done); }); @@ -27,7 +29,7 @@ describe('Body with TEXT', function () { .set('Content-Type', 'text/plain') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body, bodyText) + assert.strictEqual(res.body.request.body, bodyText) }) .expect(200, done); }); @@ -37,7 +39,7 @@ describe('Body with TEXT', function () { .set('Content-Type', 'text/plain') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body, bodyText) + assert.strictEqual(res.body.request.body, bodyText) }) .expect(200, done); }); @@ -47,7 +49,7 @@ describe('Body with TEXT', function () { .set('Content-Type', 'text/plain') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body, bodyText) + assert.strictEqual(res.body.request.body, bodyText) }) .expect(200, done); }); @@ -57,7 +59,7 @@ describe('Body with TEXT', function () { .set('Content-Type', 'text/plain') .send(bodyText) .expect(function (res) { - assert.equal(res.body.request.body, bodyText) + assert.strictEqual(res.body.request.body, bodyText) }) .expect(200, done); }); diff --git a/test/cookie.js b/test/cookie.js index 0d467cf..a0ed8c4 100644 --- a/test/cookie.js +++ b/test/cookie.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Request with Cookies', function () { var server; beforeEach(function () { @@ -15,8 +17,8 @@ describe('Request with Cookies', function () { .set('Cookie', ['testOne=valueOne;testTwo=valueTwo']) .send() .expect(function (res) { - assert.equal(res.body.request.cookies.testOne, 'valueOne') - assert.equal(res.body.request.cookies.testTwo, 'valueTwo') + assert.strictEqual(res.body.request.cookies.testOne, 'valueOne') + assert.strictEqual(res.body.request.cookies.testTwo, 'valueTwo') }) .expect(200, done); }); @@ -26,8 +28,8 @@ describe('Request with Cookies', function () { .set('Cookie', ['testOne=valueOne;testTwo=valueTwo']) .send() .expect(function (res) { - assert.equal(res.body.request.cookies.testOne, 'valueOne') - assert.equal(res.body.request.cookies.testTwo, 'valueTwo') + assert.strictEqual(res.body.request.cookies.testOne, 'valueOne') + assert.strictEqual(res.body.request.cookies.testTwo, 'valueTwo') }) .expect(200, done); }); @@ -37,8 +39,8 @@ describe('Request with Cookies', function () { .set('Cookie', ['testOne=valueOne;testTwo=valueTwo']) .send() .expect(function (res) { - assert.equal(res.body.request.cookies.testOne, 'valueOne') - assert.equal(res.body.request.cookies.testTwo, 'valueTwo') + assert.strictEqual(res.body.request.cookies.testOne, 'valueOne') + assert.strictEqual(res.body.request.cookies.testTwo, 'valueTwo') }) .expect(200, done); }); @@ -48,8 +50,8 @@ describe('Request with Cookies', function () { .set('Cookie', ['testOne=valueOne;testTwo=valueTwo']) .send() .expect(function (res) { - assert.equal(res.body.request.cookies.testOne, 'valueOne') - assert.equal(res.body.request.cookies.testTwo, 'valueTwo') + assert.strictEqual(res.body.request.cookies.testOne, 'valueOne') + assert.strictEqual(res.body.request.cookies.testTwo, 'valueTwo') }) .expect(200, done); }); @@ -59,8 +61,8 @@ describe('Request with Cookies', function () { .set('Cookie', ['testOne=valueOne;testTwo=valueTwo']) .send() .expect(function (res) { - assert.equal(res.body.request.cookies.testOne, 'valueOne') - assert.equal(res.body.request.cookies.testTwo, 'valueTwo') + assert.strictEqual(res.body.request.cookies.testOne, 'valueOne') + assert.strictEqual(res.body.request.cookies.testTwo, 'valueTwo') }) .expect(200, done); }); diff --git a/test/custom.body.js b/test/custom.body.js index b8e04ac..717108c 100644 --- a/test/custom.body.js +++ b/test/custom.body.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Custom Body', function () { var server; beforeEach(function () { @@ -14,7 +16,7 @@ describe('Custom Body', function () { .get('/') .set('X-ECHO-BODY', 'Example text') .expect(function (res) { - assert.equal(res.body, "Example text") + assert.strictEqual(res.body, "Example text") }) .expect(200, done); }); @@ -23,7 +25,7 @@ describe('Custom Body', function () { .get('/') .set('X-ECHO-BODY', '{"example": "json"}') .expect(function (res) { - assert.equal(res.body.example, "json") + assert.strictEqual(res.body.example, "json") }) .expect(200, done); }); @@ -31,7 +33,7 @@ describe('Custom Body', function () { request(server) .get('/?echo_body=Example text') .expect(function (res) { - assert.equal(res.body, "Example text") + assert.strictEqual(res.body, "Example text") }) .expect(200, done); }); @@ -39,7 +41,7 @@ describe('Custom Body', function () { request(server) .get('/?echo_body={"example": "json"}') .expect(function (res) { - assert.equal(res.body.example, "json") + assert.strictEqual(res.body.example, "json") }) .expect(200, done); }); diff --git a/test/custom.code.js b/test/custom.code.js index fd7e637..d0b1791 100644 --- a/test/custom.code.js +++ b/test/custom.code.js @@ -1,6 +1,8 @@ const request = require('supertest'); const assert = require('assert'); +process.env.LOGS__LEVEL = "error"; + describe('Custom HTTP Code', function () { var server; beforeEach(function () { @@ -98,8 +100,8 @@ describe('Custom HTTP Code', function () { .get('/') .set('X-ECHO-CODE', `${a}-${b}`) .end((err, res) => { - assert.equal(true, res.status == a || res.status == b); - assert.equal(res.header['x-echo-random-status'], `${a}, ${b}`); + assert.strictEqual(true, res.status == a || res.status == b); + assert.strictEqual(res.header['x-echo-random-status'], `${a}, ${b}`); done(); }) }); @@ -113,8 +115,8 @@ describe('Custom HTTP Code', function () { .get('/') .set('X-ECHO-CODE', `${a}-${b}-${c}`) .end((err, res) => { - assert.equal(true, res.status == a || res.status == b || res.status == c); - assert.equal(res.header['x-echo-random-status'], `${a}, ${b}, ${c}`); + assert.strictEqual(true, res.status == a || res.status == b || res.status == c); + assert.strictEqual(res.header['x-echo-random-status'], `${a}, ${b}, ${c}`); done(); }) }); @@ -127,8 +129,8 @@ describe('Custom HTTP Code', function () { request(server) .get('/?echo_code='+`${a}-${b}`) .end((err, res) => { - assert.equal(true, res.status == a || res.status == b); - assert.equal(res.header['x-echo-random-status'], `${a}, ${b}`); + assert.strictEqual(true, res.status == a || res.status == b); + assert.strictEqual(res.header['x-echo-random-status'], `${a}, ${b}`); done(); }) }); @@ -141,8 +143,8 @@ describe('Custom HTTP Code', function () { request(server) .get('/?echo_code='+`${a}-${b}-${c}`) .end((err, res) => { - assert.equal(true, res.status == a || res.status == b || res.status == c); - assert.equal(res.header['x-echo-random-status'], `${a}, ${b}, ${c}`); + assert.strictEqual(true, res.status == a || res.status == b || res.status == c); + assert.strictEqual(res.header['x-echo-random-status'], `${a}, ${b}, ${c}`); done(); }) }); @@ -154,8 +156,8 @@ describe('Custom HTTP Code', function () { .get('/') .set('X-ECHO-CODE', "200.401") .end((err, res) => { - assert.equal(200, res.status); - assert.equal(res.header['x-echo-random-status'], undefined); + assert.strictEqual(200, res.status); + assert.strictEqual(res.header['x-echo-random-status'], undefined); done(); }) }); @@ -163,8 +165,8 @@ describe('Custom HTTP Code', function () { request(server) .get('/?echo_code=200/404') .end((err, res) => { - assert.equal(200, res.status); - assert.equal(res.header['x-echo-random-status'], undefined); + assert.strictEqual(200, res.status); + assert.strictEqual(res.header['x-echo-random-status'], undefined); done(); }) }); @@ -173,8 +175,8 @@ describe('Custom HTTP Code', function () { .get('/') .set('X-ECHO-CODE', "200.401-401") .end((err, res) => { - assert.equal(401, res.status); - assert.equal(res.header['x-echo-random-status'], '401'); + assert.strictEqual(401, res.status); + assert.strictEqual(res.header['x-echo-random-status'], '401'); done(); }) }); @@ -182,8 +184,8 @@ describe('Custom HTTP Code', function () { request(server) .get('/?echo_code=200.401-404') .end((err, res) => { - assert.equal(res.status, 404); - assert.equal(res.header['x-echo-random-status'], '404'); + assert.strictEqual(res.status, 404); + assert.strictEqual(res.header['x-echo-random-status'], '404'); done(); }); }); diff --git a/test/custom.environment.js b/test/custom.environment.js index 58321cf..939d831 100644 --- a/test/custom.environment.js +++ b/test/custom.environment.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Custom Body with Environment', function () { var server; beforeEach(function () { @@ -14,7 +16,7 @@ describe('Custom Body with Environment', function () { .get('/') .set('X-ECHO-ENV-BODY', 'LANG') .expect(function (res) { - assert.equal(res.body, process.env["LANG"]) + assert.strictEqual(res.body, process.env["LANG"]) }) .expect(200, done); }); @@ -22,7 +24,7 @@ describe('Custom Body with Environment', function () { request(server) .get('/?echo_env_body=LANG') .expect(function (res) { - assert.equal(res.body, process.env["LANG"]) + assert.strictEqual(res.body, process.env["LANG"]) }) .expect(200, done); }); diff --git a/test/custom.headers.js b/test/custom.headers.js index 4263f29..34b70a2 100644 --- a/test/custom.headers.js +++ b/test/custom.headers.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Custom Headers', function () { var server; beforeEach(function () { @@ -14,7 +16,7 @@ describe('Custom Headers', function () { .get('/') .set('X-ECHO-HEADER', 'Accept-Language: en-US') .expect(function (res) { - assert.equal(res.header['accept-language'], 'en-US') + assert.strictEqual(res.header['accept-language'], 'en-US') }) .expect(200, done); }); @@ -23,7 +25,7 @@ describe('Custom Headers', function () { .get('/') .set('X-ECHO-HEADER', 'Host: en.echo-server.local:3000') .expect(function (res) { - assert.equal(res.header['host'], 'en.echo-server.local:3000') + assert.strictEqual(res.header['host'], 'en.echo-server.local:3000') }) .expect(200, done); }); @@ -32,8 +34,8 @@ describe('Custom Headers', function () { .get('/') .set('X-ECHO-HEADER', 'One: 1, Two: 2') .expect(function (res) { - assert.equal(res.header['one'], '1') - assert.equal(res.header['two'], '2') + assert.strictEqual(res.header['one'], '1') + assert.strictEqual(res.header['two'], '2') }) .expect(200, done); }); @@ -41,7 +43,7 @@ describe('Custom Headers', function () { request(server) .get('/?echo_header=Accept-Language: en-US') .expect(function (res) { - assert.equal(res.header['accept-language'], 'en-US') + assert.strictEqual(res.header['accept-language'], 'en-US') }) .expect(200, done); }); @@ -49,8 +51,8 @@ describe('Custom Headers', function () { request(server) .get('/?echo_header=One:1, Two:2') .expect(function (res) { - assert.equal(res.header['one'], '1') - assert.equal(res.header['two'], '2') + assert.strictEqual(res.header['one'], '1') + assert.strictEqual(res.header['two'], '2') }) .expect(200, done); }); diff --git a/test/custom.js b/test/custom.js index b86133e..df9cf58 100644 --- a/test/custom.js +++ b/test/custom.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Custom Body & Code', function () { var server; beforeEach(function () { @@ -15,7 +17,7 @@ describe('Custom Body & Code', function () { .set('X-ECHO-BODY', 'Oups') .set('X-ECHO-CODE', 401) .expect(function (res) { - assert.equal(res.body, "Oups") + assert.strictEqual(res.body, "Oups") }) .expect(401, done); }); @@ -23,7 +25,7 @@ describe('Custom Body & Code', function () { request(server) .get('/?echo_body=Oups&echo_code=401') .expect(function (res) { - assert.equal(res.body, "Oups") + assert.strictEqual(res.body, "Oups") }) .expect(401, done); }); @@ -33,7 +35,7 @@ describe('Custom Body & Code', function () { .set('X-ECHO-ENV-BODY', 'HOME') .set('X-ECHO-CODE', 401) .expect(function (res) { - assert.equal(res.body, process.env["HOME"]) + assert.strictEqual(res.body, process.env["HOME"]) }) .expect(401, done); }); @@ -41,7 +43,7 @@ describe('Custom Body & Code', function () { request(server) .get('/?echo_env_body=HOME&echo_code=401') .expect(function (res) { - assert.equal(res.body, process.env["HOME"]) + assert.strictEqual(res.body, process.env["HOME"]) }) .expect(401, done); }); diff --git a/test/custom.time.js b/test/custom.time.js index 7388e42..a4b1954 100644 --- a/test/custom.time.js +++ b/test/custom.time.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Custom Body', function () { var server; var time; @@ -16,7 +18,7 @@ describe('Custom Body', function () { .get('/') .expect(function (res) { var seconds = (new Date() - time) / 1000; - assert.equal(seconds < 0.5, true); + assert.strictEqual(seconds < 0.5, true); }) .expect(200, done); }); @@ -25,7 +27,7 @@ describe('Custom Body', function () { .get('/?echo_time=a') .expect(function (res) { var seconds = (new Date() - time) / 1000; - assert.equal(seconds < 0.5, true); + assert.strictEqual(seconds < 0.5, true); }) .expect(200, done); }); @@ -34,7 +36,7 @@ describe('Custom Body', function () { .get('/?echo_time=1000') .expect(function (res) { var seconds = (new Date() - time) / 1000; - assert.equal(seconds >= 1, true); + assert.strictEqual(seconds >= 1, true); }) .expect(200, done); }); @@ -43,7 +45,7 @@ describe('Custom Body', function () { .get('/?echo_time=500') .expect(function (res) { var seconds = (new Date() - time) / 1000; - assert.equal(seconds >= 0.5, true); + assert.strictEqual(seconds >= 0.5, true); }) .expect(200, done); }); @@ -53,7 +55,7 @@ describe('Custom Body', function () { .set('X-ECHO-TIME', '1000') .expect(function (res) { var seconds = (new Date() - time) / 1000; - assert.equal(seconds >= 1, true); + assert.strictEqual(seconds >= 1, true); }) .expect(200, done); }); @@ -63,7 +65,7 @@ describe('Custom Body', function () { .set('X-ECHO-TIME', '500') .expect(function (res) { var seconds = (new Date() - time) / 1000; - assert.equal(seconds >= 0.5, true); + assert.strictEqual(seconds >= 0.5, true); }) .expect(200, done); }); diff --git a/test/environment.js b/test/environment.js index 6da67c6..8e51158 100644 --- a/test/environment.js +++ b/test/environment.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Environment', function () { var server; beforeEach(function () { @@ -13,7 +15,7 @@ describe('Environment', function () { request(server) .get('/') .expect(function (res) { - assert.equal(res.body.environment[0], process.env[0]) + assert.strictEqual(res.body.environment[0], process.env[0]) }) .expect(200, done); }); diff --git a/test/file.js b/test/file.js index 2af08e7..245ab49 100644 --- a/test/file.js +++ b/test/file.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('File', function () { var server; beforeEach(function () { diff --git a/test/headers.js b/test/headers.js index f0d0b09..76bb0c8 100644 --- a/test/headers.js +++ b/test/headers.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Headers', function () { var server; beforeEach(function () { @@ -14,7 +16,7 @@ describe('Headers', function () { .get('/') .set('test', 'ok') .expect(function (res) { - assert.equal(res.body.request.headers['test'], 'ok') + assert.strictEqual(res.body.request.headers['test'], 'ok') }) .expect(200, done); }); @@ -23,7 +25,7 @@ describe('Headers', function () { .post('/') .set('test', 'ok') .expect(function (res) { - assert.equal(res.body.request.headers['test'], 'ok') + assert.strictEqual(res.body.request.headers['test'], 'ok') }) .expect(200, done); }); @@ -32,7 +34,7 @@ describe('Headers', function () { .put('/') .set('test', 'ok') .expect(function (res) { - assert.equal(res.body.request.headers['test'], 'ok') + assert.strictEqual(res.body.request.headers['test'], 'ok') }) .expect(200, done); }); @@ -41,7 +43,7 @@ describe('Headers', function () { .patch('/') .set('test', 'ok') .expect(function (res) { - assert.equal(res.body.request.headers['test'], 'ok') + assert.strictEqual(res.body.request.headers['test'], 'ok') }) .expect(200, done); }); @@ -50,7 +52,7 @@ describe('Headers', function () { .delete('/') .set('test', 'ok') .expect(function (res) { - assert.equal(res.body.request.headers['test'], 'ok') + assert.strictEqual(res.body.request.headers['test'], 'ok') }) .expect(200, done); }); diff --git a/test/query.js b/test/query.js index 578764d..5eefb1d 100644 --- a/test/query.js +++ b/test/query.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('Request with Query', function () { var server; beforeEach(function () { @@ -13,7 +15,7 @@ describe('Request with Query', function () { request(server) .get('/?test=ok') .expect(function (res) { - assert.equal(res.body.request.query.test, 'ok') + assert.strictEqual(res.body.request.query.test, 'ok') }) .expect(200, done); }); @@ -21,7 +23,7 @@ describe('Request with Query', function () { request(server) .post('/?test=ok') .expect(function (res) { - assert.equal(res.body.request.query.test, 'ok') + assert.strictEqual(res.body.request.query.test, 'ok') }) .expect(200, done); }); @@ -29,7 +31,7 @@ describe('Request with Query', function () { request(server) .put('/?test=ok') .expect(function (res) { - assert.equal(res.body.request.query.test, 'ok') + assert.strictEqual(res.body.request.query.test, 'ok') }) .expect(200, done); }); @@ -37,7 +39,7 @@ describe('Request with Query', function () { request(server) .patch('/?test=ok') .expect(function (res) { - assert.equal(res.body.request.query.test, 'ok') + assert.strictEqual(res.body.request.query.test, 'ok') }) .expect(200, done); }); @@ -45,7 +47,7 @@ describe('Request with Query', function () { request(server) .delete('/?test=ok') .expect(function (res) { - assert.equal(res.body.request.query.test, 'ok') + assert.strictEqual(res.body.request.query.test, 'ok') }) .expect(200, done); }); diff --git a/test/verbs.js b/test/verbs.js index e6d5dec..2a10caf 100644 --- a/test/verbs.js +++ b/test/verbs.js @@ -1,6 +1,8 @@ const assert = require('assert'); const request = require('supertest'); +process.env.LOGS__LEVEL = "error"; + describe('HTTP Verbs', function () { var server; beforeEach(function () { @@ -13,10 +15,10 @@ describe('HTTP Verbs', function () { request(server) .get('/') .expect(function (res) { - assert.equal(res.body.http.method, 'GET') - assert.equal(res.body.http.protocol, 'http') - assert.equal(res.body.http.originalUrl, '/') - assert.equal(res.body.http.baseUrl, '') + assert.strictEqual(res.body.http.method, 'GET') + assert.strictEqual(res.body.http.protocol, 'http') + assert.strictEqual(res.body.http.originalUrl, '/') + assert.strictEqual(res.body.http.baseUrl, '') }) .expect(200, done); }); @@ -24,10 +26,10 @@ describe('HTTP Verbs', function () { request(server) .post('/') .expect(function (res) { - assert.equal(res.body.http.method, 'POST') - assert.equal(res.body.http.protocol, 'http') - assert.equal(res.body.http.originalUrl, '/') - assert.equal(res.body.http.baseUrl, '') + assert.strictEqual(res.body.http.method, 'POST') + assert.strictEqual(res.body.http.protocol, 'http') + assert.strictEqual(res.body.http.originalUrl, '/') + assert.strictEqual(res.body.http.baseUrl, '') }) .expect(200, done); }); @@ -35,10 +37,10 @@ describe('HTTP Verbs', function () { request(server) .put('/') .expect(function (res) { - assert.equal(res.body.http.method, 'PUT') - assert.equal(res.body.http.protocol, 'http') - assert.equal(res.body.http.originalUrl, '/') - assert.equal(res.body.http.baseUrl, '') + assert.strictEqual(res.body.http.method, 'PUT') + assert.strictEqual(res.body.http.protocol, 'http') + assert.strictEqual(res.body.http.originalUrl, '/') + assert.strictEqual(res.body.http.baseUrl, '') }) .expect(200, done); }); @@ -46,10 +48,10 @@ describe('HTTP Verbs', function () { request(server) .patch('/') .expect(function (res) { - assert.equal(res.body.http.method, 'PATCH') - assert.equal(res.body.http.protocol, 'http') - assert.equal(res.body.http.originalUrl, '/') - assert.equal(res.body.http.baseUrl, '') + assert.strictEqual(res.body.http.method, 'PATCH') + assert.strictEqual(res.body.http.protocol, 'http') + assert.strictEqual(res.body.http.originalUrl, '/') + assert.strictEqual(res.body.http.baseUrl, '') }) .expect(200, done); }); @@ -57,10 +59,10 @@ describe('HTTP Verbs', function () { request(server) .delete('/') .expect(function (res) { - assert.equal(res.body.http.method, 'DELETE') - assert.equal(res.body.http.protocol, 'http') - assert.equal(res.body.http.originalUrl, '/') - assert.equal(res.body.http.baseUrl, '') + assert.strictEqual(res.body.http.method, 'DELETE') + assert.strictEqual(res.body.http.protocol, 'http') + assert.strictEqual(res.body.http.originalUrl, '/') + assert.strictEqual(res.body.http.baseUrl, '') }) .expect(200, done); });