diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fdd3df9 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,32 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach to user-simulator container", + "address": "localhost", + "port": 9230, + "protocol": "inspector", + "localRoot": "${workspaceFolder}/user-simulator/src", + "remoteRoot": "/usr/src/app", + "restart": true + }, + { + "type": "node", + "request": "attach", + "name": "Attach to api-server container", + // should match the exposed debug address specified in docker-compose + "address": "localhost", + "port": 9229, + "protocol": "inspector", + // you typically copy the src folder into a location in docker...specify this mapping. + "localRoot": "${workspaceFolder}/api-server/src", + "remoteRoot": "/usr/src/app", + "restart": true + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index abfe097..b26a030 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,94 @@ # Horus -Monitoring containerized microservices with a centralized logging architecture. +A project for learning about microservices observability. -## Dependencies +* Centralized Logging +* Distributed Tracing + +## Development Requirements - Docker Compose v1.23.2 +- OSX or GNU/Linux + +## Development Setup + +1. Clone this repo and open the root folder in an IDE like *Visual Studio Code*. + +2. For each microservice, rename `example.env` to `.env` and supply the needed secrets. + > TODO: Eliminate this friction. + +3. Start all microservices in *development mode*. + + docker-compose -f docker-compose.dev.yml \ + up -d --build + + > In Development Mode + > + > - You can attach a remote debugger to a running service Docker for seemless debugging like placing breakpoints and watching variables. + > - Changes to source files will automatically restart the corresponding docker service. + +4. Optionally, attach the IDE's debugger to a service as follows in Visual Studio Code: *shift+cmd+D > Select a debug configuration > F5*. + > All vscode debug configurations are stored in *.vscode/launch.json*. You can modify configs as you see fit. + +5. Visit http://localhost:16686 to view traces. + +### Useful dev commands -## Setup + # list all running services + docker-compose -f docker-compose.dev.yml ps -1. Signup with an ELK SaaS provider like [Logz.io](logz.io) to obtain an authentication token. Then for each microservice, rename `example.env` to `.env` and supply the needed secrets. + # stop all services + docker-compose -f docker-compose.dev.yml down -2. Run the following commands. + # restart all [or specific] service + docker-compose -f docker-compose.dev.yml \ + up -d --no-deps --build [service-name] - docker-compose build --pull - docker-compose up -d --force-recreate - -3. Then log into your ELK SaaS and view your microservices logs. + # tail logs from all [or specific] service + docker-compose -f docker-compose.dev.yml \ + logs -f [service-name] + + # see how an image was built + docker history -## Project Documentation +## Project Architecture -### System Architecture +### Logging Infrastructure ![](docs/container-architecture.svg) -I wrote an accompanying [article](https://hackernoon.com/monitoring-containerized-microservices-with-a-centralized-logging-architecture-ba6771c1971a) explaining this architecture. +Read this [article](https://hackernoon.com/monitoring-containerized-microservices-with-a-centralized-logging-architecture-ba6771c1971a) for more details. -## Notes +### Tracing Infrastructure -### Docker Networking +![Tracing Backend Architecture](docs/distributed-tracing/tracing-backend.svg) + +Read this [article](#todo) for more details. + +## Miscellaneous Notes + +### TODO (Improvement Considerations) + +- Research **jaeger-operator** -By default each containerized process runs in an isolated network namespace. For inter-container communication, place them in the same network namespace...as seen in *docker-compose.yml*. +- Name Duplication: The value of the `API_SERVER_ADDRESS` variable in *user-simulator/.env* depends on the service name `api-server` specified in *docker-compose.yml*. If we rename the service, we must also change the variable. Is there a way to make this DRY? + +- In the log-shipper container, I had to install a logz.io-specific plugin. Can't this step be eliminated since fluentd is capable of connecting to https endpoints without plugins? + +- Use sub-second precision for fluentd timestamps (probably best to use nanoseconds.) ### Best practices 1. You can pass secrets for a microservice using the `env_file` attribute in *docker-compose.yml*. 2. Microservices can communicate using their service names if they are in the same docker network. -### Improvement Considerations +### Docker Networking + +By default each containerized process runs in an isolated network namespace. For inter-container communication, place them in the same network namespace. -1. **Name Duplication:** The value of the `API_SERVER_ADDRESS` variable in *user-simulator/.env* depends on the service name `api-server` specified in *docker-compose.yml*. If we rename the service, we must also change the variable. Is there a way to make this DRY? +### References -2. In the log-shipper container, I had to install a logz.io-specific plugin. Can't this step be eliminated since fluentd is capable of connecting to https endpoints without plugins? \ No newline at end of file +- https://medium.com/lucjuggery/docker-in-development-with-nodemon-d500366e74df +- https://blog.risingstack.com/how-to-debug-a-node-js-app-in-a-docker-container/ +- https://codefresh.io/docker-tutorial/debug_node_in_docker/ +- https://code.visualstudio.com/docs/editor/debugging \ No newline at end of file diff --git a/api-server/.dockerignore b/api-server/.dockerignore index b44764a..4125497 100644 --- a/api-server/.dockerignore +++ b/api-server/.dockerignore @@ -1,4 +1,4 @@ .env -/node_modules -npm-debug.log +node_modules +yarn-error.log .DS_Store \ No newline at end of file diff --git a/api-server/.gitignore b/api-server/.gitignore index b44764a..c796e7b 100644 --- a/api-server/.gitignore +++ b/api-server/.gitignore @@ -1,4 +1,4 @@ .env -/node_modules +node_modules npm-debug.log .DS_Store \ No newline at end of file diff --git a/api-server/Dockerfile b/api-server/Dockerfile index 21b4261..c504f03 100644 --- a/api-server/Dockerfile +++ b/api-server/Dockerfile @@ -2,12 +2,19 @@ FROM node:10.10.0-alpine WORKDIR /usr/src/app -# optimization to only rebuild npm modules iff package[-lock].json changes -COPY src/package*.json ./ +RUN apk add --no-cache bash curl \ + && yarn global add nodemon -# For production `RUN npm install --only=production` -RUN npm install +COPY wait.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/wait.sh + +COPY src/package.json src/yarn.lock ./ +RUN yarn install COPY src . -CMD ["npm", "start"] \ No newline at end of file +CMD ["node", "."] + +# ref: https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md +# https://nodejs.org/en/docs/guides/nodejs-docker-webapp/ +# https://runnable.com/blog/9-common-dockerfile-mistakes \ No newline at end of file diff --git a/api-server/README.md b/api-server/README.md index 4f8d3c5..d1b66a3 100644 --- a/api-server/README.md +++ b/api-server/README.md @@ -13,4 +13,16 @@ docker logs -f # From another terminal on the host machine, test the server - curl http://localhost:5000/api/v1/tokens \ No newline at end of file + curl http://localhost:5000/api/v1/tokens + +## Dev Notes + +To quickly test a container + + docker build -t --no-cache . \ + && docker run -it -v $(pwd)/src:/usr/src/app sh + + docker build -t api-server --no-cache . \ + && docker run -it -v $(pwd)/src:/usr/src/app api-server sh + +> The `-v` flag maps host absolute path to container absolute path. \ No newline at end of file diff --git a/api-server/example.env b/api-server/example.env index 8f55cbd..f87f5c1 100644 --- a/api-server/example.env +++ b/api-server/example.env @@ -1 +1 @@ -PORT= \ No newline at end of file +# TODO \ No newline at end of file diff --git a/api-server/src/app.js b/api-server/src/app.js index 1f11e70..4e0c538 100644 --- a/api-server/src/app.js +++ b/api-server/src/app.js @@ -1,19 +1,84 @@ -// const dotenv = require('dotenv') -// if (process.env.NODE_ENV !== 'production') { -// const result = dotenv.config() -// if (result.error) { -// throw result.error -// } -// } - const app = require('express')() const uuid = require('uuid/v1') +const axios = require('axios') + +// For portability, we initialize tracer from envars instead of local options. +// See: https://www.npmjs.com/package/jaeger-client#environment-variables +var opentracing = require('opentracing') +var initTracer = require('jaeger-client').initTracerFromEnv; +var tracer = initTracer() + +app.use('/health', (req, res) => { + res.json(null) +}) app.use('/api/v1/tokens', (req, res) => { + const span = tracer.startSpan('token-request') + console.log(`Handling request for token`) res.json({token: uuid()}) + + span.finish() }) -const server = app.listen(process.env.PORT, () => { +app.use('/api/v1/whereami', async (req, res, next) => { + const parentSpan = createContinuationSpan(tracer, req, 'whereami-request') + + try { + // get location of IP Address + const IP = '23.16.76.104' + const locSpan = tracer.startSpan('get-location', {childOf: parentSpan}) + const location = await axios.get(`http://ip-api.com/json/${IP}`) + locSpan.finish() + const {lat, lon, city, country} = location.data + + // do some other async task + const fakeSpan = tracer.startSpan('get-weather', {childOf: parentSpan}) + const _ = await fakeFetch(1500, 0.7) + fakeSpan.finish() + + // return results + const data = {lat: lat, lon: lon, city: city, country: country} + res.json(data) + } catch(err) { + parentSpan.setTag('ERROR', err) + next(err) + } + + parentSpan.finish() +}) + +const server = app.listen(process.env.PORT || 3000, () => { console.log(`Listening on ${ server.address().address }:${ server.address().port }`) -}) \ No newline at end of file +}) + +function fakeFetch(msDelay, successRate) { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (Math.random() <= successRate) { + resolve() + } else { + reject('Fake fetch failed randomly.') + } + }, msDelay) + }) +} + +function extractContext(tracer, req) { + return tracer.extract(opentracing.FORMAT_HTTP_HEADERS, req.headers) +} + +// If the request is already being traced, continue the trace +// else start a new trace. +function createContinuationSpan(tracer, req, spanName) { + const incomingSpanContext = extractContext(tracer, req) + + let newSpan = null + if (incomingSpanContext == null) { + newSpan = tracer.startSpan(spanName) + } else { + newSpan = tracer.startSpan(spanName, {childOf: incomingSpanContext}) + } + + return newSpan +} \ No newline at end of file diff --git a/api-server/src/package-lock.json b/api-server/src/package-lock.json deleted file mode 100644 index fdae678..0000000 --- a/api-server/src/package-lock.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "name": "api-server", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "accepts": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", - "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", - "requires": { - "mime-types": "~2.1.18", - "negotiator": "0.6.1" - } - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" - }, - "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.1", - "http-errors": "~1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "~2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "~1.6.15" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" - }, - "dotenv": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.0.0.tgz", - "integrity": "sha512-FlWbnhgjtwD+uNLUGHbMykMOYQaTivdHEmYwAKFjn6GKe/CqY0fNae93ZHTd20snh9ZLr8mTzIL9m0APQ1pjQg==" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.16.3", - "resolved": "http://registry.npmjs.org/express/-/express-4.16.3.tgz", - "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", - "requires": { - "accepts": "~1.3.5", - "array-flatten": "1.1.1", - "body-parser": "1.18.2", - "content-disposition": "0.5.2", - "content-type": "~1.0.4", - "cookie": "0.3.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.1.1", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.3", - "qs": "6.5.1", - "range-parser": "~1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", - "unpipe": "~1.0.0" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ipaddr.js": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", - "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "mime-db": { - "version": "1.36.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", - "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" - }, - "mime-types": { - "version": "2.1.20", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", - "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", - "requires": { - "mime-db": "~1.36.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", - "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } - }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "proxy-addr": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", - "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.8.0" - } - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" - }, - "range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" - }, - "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "unpipe": "1.0.0" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", - "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" - } - }, - "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - }, - "type-is": { - "version": "1.6.16", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", - "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.18" - } - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - } - } -} diff --git a/api-server/src/package.json b/api-server/src/package.json index 1993dd2..e107cae 100644 --- a/api-server/src/package.json +++ b/api-server/src/package.json @@ -10,8 +10,10 @@ "author": "Uzziah Eyee", "license": "ISC", "dependencies": { + "axios": "^0.18.0", "dotenv": "^6.0.0", "express": "^4.16.3", + "jaeger-client": "^3.14.4", "uuid": "^3.3.2" } } diff --git a/api-server/src/yarn.lock b/api-server/src/yarn.lock new file mode 100644 index 0000000..ecd21c1 --- /dev/null +++ b/api-server/src/yarn.lock @@ -0,0 +1,391 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.3.5: + version "1.3.6" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.6.tgz#27de8682f0833e966dde5c5d7a63ec8523106e4b" + dependencies: + mime-types "~2.1.24" + negotiator "0.6.1" + +ansi-color@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-color/-/ansi-color-0.2.1.tgz#3e75c037475217544ed763a8db5709fa9ae5bf9a" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +axios@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" + dependencies: + follow-redirects "^1.3.0" + is-buffer "^1.1.5" + +body-parser@1.18.3: + version "1.18.3" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "~1.6.3" + iconv-lite "0.4.23" + on-finished "~2.3.0" + qs "6.5.2" + raw-body "2.3.3" + type-is "~1.6.16" + +bufrw@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bufrw/-/bufrw-1.2.1.tgz#93f222229b4f5f5e2cd559236891407f9853663b" + dependencies: + ansi-color "^0.2.1" + error "^7.0.0" + xtend "^4.0.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + dependencies: + ms "^2.1.1" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +dotenv@^6.0.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + +error@7.0.2, error@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" + dependencies: + string-template "~0.2.1" + xtend "~4.0.0" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + +express@^4.16.3: + version "4.16.4" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" + dependencies: + accepts "~1.3.5" + array-flatten "1.1.1" + body-parser "1.18.3" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.1" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.4" + qs "6.5.2" + range-parser "~1.2.0" + safe-buffer "5.1.2" + send "0.16.2" + serve-static "1.13.2" + setprototypeof "1.1.0" + statuses "~1.4.0" + type-is "~1.6.16" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.4.0" + unpipe "~1.0.0" + +follow-redirects@^1.3.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + dependencies: + debug "^3.2.6" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +iconv-lite@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +jaeger-client@^3.14.4: + version "3.15.0" + resolved "https://registry.yarnpkg.com/jaeger-client/-/jaeger-client-3.15.0.tgz#01e38937aa161d3118bdd685dd1b1eabab6bcf5e" + dependencies: + node-int64 "^0.4.0" + opentracing "^0.13.0" + thriftrw "^3.5.0" + uuid "^3.2.1" + xorshift "^0.2.0" + +long@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/long/-/long-2.4.0.tgz#9fa180bb1d9500cdc29c4156766a1995e1f4524f" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + +mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + dependencies: + mime-db "1.40.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +opentracing@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.13.0.tgz#6a341442f09d7d866bc11ed03de1e3828e3d6aab" + +parseurl@~1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +proxy-addr@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +qs@6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" + dependencies: + bytes "3.0.0" + http-errors "1.6.3" + iconv-lite "0.4.23" + unpipe "1.0.0" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +send@0.16.2: + version "0.16.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.4.0" + +serve-static@1.13.2: + version "1.13.2" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + +statuses@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + +string-template@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" + +thriftrw@^3.5.0: + version "3.11.3" + resolved "https://registry.yarnpkg.com/thriftrw/-/thriftrw-3.11.3.tgz#2cef6b4d089b7ba6275198b86582881582907d45" + dependencies: + bufrw "^1.2.1" + error "7.0.2" + long "^2.4.0" + +type-is@~1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + +uuid@^3.2.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +xorshift@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/xorshift/-/xorshift-0.2.1.tgz#fcd82267e9351c13f0fb9c73307f25331d29c63a" + +xtend@^4.0.0, xtend@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" diff --git a/api-server/wait.sh b/api-server/wait.sh new file mode 100644 index 0000000..b952e08 --- /dev/null +++ b/api-server/wait.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Requires a container with Bash and Curl installed. + +is_healthy () { + retryInterval=${2:-5} + maxAttempts=${3:-1000000} + + i=0 + while [ $i -lt $maxAttempts ] + do + status=$(curl -s -L -o /dev/null -w %{http_code} $1) + + # stop trying if we get a success response + if [ $status -ge 200 ] && [ $status -lt 300 ] + then + return 0 + fi + + ((i++)) + echo "Attempt $i of $maxAttempts: Endpoint $1 returned code $status. Retrying in $retryInterval seconds." + sleep $retryInterval + done + + return 1 +} + +# args: endpoint, retryInterval (secs), maxAttempts, command +if is_healthy $1 $2 $3 +then + echo "Running command: $4" + # vulnerable to input-injection attacts (but I don't care :) + eval "$4" +else + echo "Exiting: Cannot run command because health-check failed." +fi \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..2131de8 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,118 @@ +version: '3' + +services: + user-simulator: + build: user-simulator + env_file: user-simulator/.env + environment: + NODE_ENV: development + JAEGER_SERVICE_NAME: user-simulator + JAEGER_AGENT_HOST: jaeger-agent + JAEGER_AGENT_PORT: 6832 + JAEGER_SAMPLER_TYPE: const + JAEGER_SAMPLER_PARAM: 1 + depends_on: + - api-server + - log-shipper + ports: + - "9230:9229" # nodejs debugging port + command: wait.sh http://api-server:3000/health 5 20 "nodemon --inspect=0.0.0.0 ." + volumes: + - ./user-simulator/src:/usr/src/app + networks: + - aparnet + + api-server: + build: api-server + env_file: api-server/.env + environment: + #todo: change syntax to 'var: val' + PORT: 3000 + JAEGER_SERVICE_NAME: api-server + # https://www.jaegertracing.io/docs/1.11/sampling/#client-sampling-configuration + JAEGER_SAMPLER_TYPE: const + JAEGER_SAMPLER_PARAM: 1 + # by default, jaeger-client sends traces to an agent on localhost:6832 + # override and send traces to http://tracing-backend:6832 + JAEGER_AGENT_HOST: jaeger-agent + JAEGER_AGENT_PORT: 6832 + ports: + - "3000:3000" # for receiving api requests + - "9229:9229" # nodejs debugging port + # override the dockerfile startup command to: + # 1) restart on file changes using nodemon + # 2) start nodejs in debug mode and allow connections from all IPs + # our healthcheck hack is to request the current sampling strategy. + command: wait.sh http://jaeger-agent:5778?service 5 20 "nodemon --inspect=0.0.0.0 ." + volumes: + - ./api-server/src:/usr/src/app + networks: + - aparnet + depends_on: + - log-shipper + - jaeger-agent + + log-shipper: + build: log-shipper + ports: + - "24224:24224" + - "24224:24224/udp" + env_file: log-shipper/.env + + jaeger-agent: + build: jaeger-agent + environment: + # more options: https://www.jaegertracing.io/docs/1.11/deployment/#all-options-1 + REPORTER_TYPE: grpc + REPORTER_GRPC_HOST_PORT: jaeger-collector:14250 + PROCESSOR_JAEGER_BINARY_SERVER_HOST_PORT: :6832 + command: wait.sh http://jaeger-collector:14269 5 20 "jaeger-agent" + ports: + - "6832:6832" # receiving traces via UDP + - "5778:5778" # get sampling strategies + networks: + - aparnet + depends_on: + - jaeger-collector + + jaeger-collector: + build: jaeger-collector + environment: + # more options: https://www.jaegertracing.io/docs/1.11/deployment/#all-options-1 + SPAN_STORAGE_TYPE: elasticsearch + ES_SERVER_URLS: http://elasticsearch:9200 + restart: on-failure + # todo: eliminate duplication of health-check url in 'command' + command: wait.sh http://elasticsearch:9200 5 20 "jaeger-collector" + ports: + - "14269:14269" # healthcheck + - "14250:14250" # for receiving spans via grpc + networks: + - aparnet + depends_on: + - elasticsearch + + jaeger-query: + build: jaeger-query + environment: + SPAN_STORAGE_TYPE: elasticsearch + ES_SERVER_URLS: http://elasticsearch:9200 + ports: + - "16686:16686" # provides UI + - "16687:16687" # healthcheck + networks: + - aparnet + command: wait.sh http://elasticsearch:9200 5 20 "jaeger-query" + depends_on: + - elasticsearch + + elasticsearch: + image: elasticsearch:5.6-alpine + ports: + - "9200:9200" # server listens here + - "9300:9300" + networks: + - aparnet + +networks: + aparnet: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.prod.yml similarity index 100% rename from docker-compose.yml rename to docker-compose.prod.yml diff --git a/docs/distributed-tracing/Monitoring Microservices Through Distributed Tracing..md b/docs/distributed-tracing/Monitoring Microservices Through Distributed Tracing..md new file mode 100644 index 0000000..e1a0463 --- /dev/null +++ b/docs/distributed-tracing/Monitoring Microservices Through Distributed Tracing..md @@ -0,0 +1,251 @@ +# Microservices Observability with Distributed Tracing. +A continued study of Project Horus. + +## Introduction + +Generally speaking, there are 3 ways of collecting data for observing microservices: *Metrics*, *Logging*, and *Distributed tracing* [^ben1]. In an earlier article I examined an architecture for setting up logging on our running example *Project Horus* [^first_article]. Now we continue our journey on observability by adding distributed tracing to our microservices. + +In addition to examining the theory and principles of distributed tracing, we will setup a complete tracing pipeline for observing two Node.js microservices running in a Docker environment. The core principles are platform agonositic and can indeed be used on heterogenous services running in a Kubernetes cluster. + +However, beware that observability involves more than capturing traces, we must also be able to interpret this data for timely business decisions. This is an ongoing area of research that this article will only briefly cover. + +## What is Distributed Tracing? + +In simple terms, Distributed Tracing is about understanding the path of a request as it propagates through the microservices in our application. While logging can inform us of important events when servicing a request, tracing tells us the full story about how that request was handled across all services from start to finish. + +As an analogy, logs are like alerts on a roadtrip. You only want to be notified when something important happens like road closures and severe weather conditions. But tracing is the complete route of your journey: what towns you visited, what roads you took etc. Indeed you could enrich your trace by annotating it with logs of important events. + +But tracking requests across heterogeneous services presents unique challenges, especially at scale. We want a solution that is portable, simple to implement, and with little performance overhead. The *OpenTracing* project was created to tackle these issues [^opentracing1]. + +## The Terminology of Opentracing + +![Multiple Views of a Trace](./multiple-views-of-a-trace.svg) + +The above images show various perspectives of our sample application. The application allows a user to request information about their location. The request is handled by an *api-service*, which first translates the user's IP address to a city name using a 3rd party *ip-service*. Secondly, it obtains the latest weather information for that city from a 3rd party *weather-service*. Finally the combined information is returned to the user. + +A *Transaction* is an end-to-end request-response flow, i.e from making the user's initial request to receiving the final location and weather response. A transaction often involves the interaction of multiple services—Fig 2 shows 4 services collaborating to enable one transaction. + +A *Trace* is the record of a transaction. It captures the work done by each service as *Spans*—Fig 3 shows a trace with 4 spans. While a span is unfinished, we could do some external work and capture these as children spans. Hence as Fig 4 shows, a trace can also be viewed as a directed acyclic graph (DAG) of spans. + +A *SpanContext* is perhaps the most important concept to understand. It is the edge that connects 2 spans to form a DAG as in Fig 4. Practically, it is a reference to a parent span. When propagating a trace from one service to another, you *inject* the SpanContext into the request's headers and extract them in the receiving service, then you can create a child span referencing the extracted parent ID. + +The Opentracing Specification provides a more technical description of these concepts[^opentracing_spec], but the above overview is sufficient to proceed with setting up tracing. + +## Setting up a tracing pipeline + +Setting up tracing involves 3 steps: acquiring a trace, storing the trace, and later visualizing the trace. The image below shows the two alternative approaches for setting up of a tracing pipeline. Generally, you instrument your service with a client library that provides a Tracer object with which you can create and link Spans. The client library then sends the tracing data to either an agent or a cloud endpoint. Eventually the traces are stored in a database like Elasticsearch, and can be queried using a UI client. + +![Two general tracing pipelines](two-tracing-pipelines.svg) + +The strategy using a Tracing SaaS provider is obviously simpler as it offloads the complexity of managing a tracing agent, database, and query UI. However, I've chosen the alternative self-managed approach highlighted in green. Just so we can understand how everything works behind the scenes. + +### Step 1: Acquiring traces using the Jaeger Client + +You acquire a trace by instrumenting your services with a client library that captures Opentracing-compliant tracing data. We would be using the *jaeger-client* [^jaeger-client] package which implements the Opentracing 1.1 API by extending the *opentracing* [^opentracing-pkg] reference implementation. The Jaeger Client library exposes a Tracer object which we use to construct and send spans to a tracing backend. + +Some alternative client libraries are *lightstep-tracer* [^lightstep-pkg] and *elastic-apm-node-opentracing* [^elastic-pkg]. The image below shows various instrumentation paths we could take, I have chosen to follow the path highlighted in green. + +![](client-instrumentation-structure.svg) + +First we need to import a *Tracer* object provided by *jaeger-client* in both services. The tracer will be used to create spans later. + +```js +/* in each service */ + +var opentracing = require('opentracing') +var initTracer = require('jaeger-client').initTracerFromEnv; +var tracer = initTracer() +``` +> Notice how the Tracer is initialized from environment variables. This is a best practice for enabling easy tracing configuration from a higher level like a Docker Compose file. + +Then we use the tracer object to create spans. But before making a request, we propagate the span's *SpanContext* as HTTP headers as shown below in `Line 7`. + +```js +/* user-simulator */ + +async function whereami() => { + const span = tracer.startSpan('whereami-request') + + const location = await axios.get(`${process.env.API_SERVER_ADDRESS}/whereami`, { + headers: getCarrier(span, tracer) + }) + + span.finish() +}) + +function getCarrier(span, tracer) { + const carrier = {} + tracer.inject(span.context(), opentracing.FORMAT_HTTP_HEADERS, carrier) + return carrier +} + +``` +On the receiving service, if the incoming request is already being traced, we want our span to be a child of the incoming span, else we create a root span. We handle this with the helper function `createContinuationSpan()`. + +```js +/* api-service */ + +app.use('/api/v1/whereami', async (req, res, next) => { + const parentSpan = createContinuationSpan(tracer, req, 'whereami-request') + + const childSpan = tracer.startSpan('city-from-ip', {childOf: parentSpan}) + const _ = await axios.get(`http://ip-api.com/json/${req.ip}`) + childSpan.finish() + + // Do more work and create more children spans. + + parentSpan.finish() +}) + +function createContinuationSpan(tracer, req, spanName) { + const incomingSpanContext = tracer.extract(opentracing.FORMAT_HTTP_HEADERS, req.headers) + + if (incomingSpanContext == null) { + return tracer.startSpan(spanName) + } + + return tracer.startSpan(spanName, {childOf: incomingSpanContext}) +} +``` +And that's how we propagate traces from one service to another. However, as you'd quickly find, this manual span creation and linking is verbose and can quickly become a maintenance headache. So, we could write custom request-response handlers and automatically inject/extract span information. But that's an optional optimization. Let's now consider how to save and visualize these traces. + +### Step 2 & 3: Storing and visualizing traces with the Jaeger tracing-backend. + +We collect, store, and query the traces using a couple of components collectively referred to as the *tracing-backend*. The image below shows the 4 components of the Jaeger tracing-backend. + +1. **jaeger-agent** typically runs as a deamon or sidecar container alongside an instrumented service. The agent receives traces from the jaeger-client over UDP and forwards them to the jaeger-collector. An agent is not strictly necessary because we can send traces directly to the collector. However, using an agent helps abstract away batching and collector discovery logic from the jaeger-client.[^jaeger-agent] + +2. **jaeger-collector** handles the logic of storing traces in persistent storage. It supports a couple of *storage-backends* for interfacing with various database types like Cassandra, Elasticsearch, Kafka, or plain memory.[^jaeger-collector] + +3. **Elasticsearch** is a high-perfomant database based on Apache Lucene that supports indexing and searching of massive datasets with near-realtime responsiveness. [^elasticsearch] + +4. **Jaeger Query and UI** is the final piece of the puzzle, we use it to search and visualize our trace data.[^jaeger-query] + +![The Jaeger Tracing Backend](tracing-backend.svg) + +There are many ways you could run these components. For instance, the *jaegertracing/all-in-one* [^jaeger-allinone] image enables you to run the entire tracing backend in a single container (traces will be stored in memory). However, in production you'd want to run each component as a separate service for better scalability. The snippet below shows my abridged *docker-compose.yml* file, where I've chosen to follow the latter approach. + + +```yaml +version: '3' + +services: + user-simulator: # an instrumented service + build: user-simulator + environment: + JAEGER_SERVICE_NAME: user-simulator + JAEGER_AGENT_HOST: jaeger-agent + JAEGER_AGENT_PORT: 6832 + JAEGER_SAMPLER_TYPE: const + JAEGER_SAMPLER_PARAM: 1 + depends_on: + - jaeger-agent + + jaeger-agent: + build: jaeger-agent + environment: + REPORTER_TYPE: grpc + REPORTER_GRPC_HOST_PORT: jaeger-collector:14250 + PROCESSOR_JAEGER_BINARY_SERVER_HOST_PORT: :6832 + ports: + - "6832:6832" # for receiving traces via UDP + depends_on: + - jaeger-collector + + jaeger-collector: + build: jaeger-collector + environment: + SPAN_STORAGE_TYPE: elasticsearch + ES_SERVER_URLS: http://elasticsearch:9200 + ports: + - "14250:14250" # for receiving spans via grpc + depends_on: + - elasticsearch + + jaeger-query: + build: jaeger-query + environment: + SPAN_STORAGE_TYPE: elasticsearch + ES_SERVER_URLS: http://elasticsearch:9200 + ports: + - "16686:16686" # provides UI + depends_on: + - elasticsearch + + elasticsearch: + image: elasticsearch:5.6-alpine + ports: + - "9200:9200" # server listens here +``` + +#### An important note on service dependencies in Docker. + +Some services depend on another service to be *ready* before they can start. In our system diagram above the arrows show that *jaeger-collector* depends on *elasticsearch*, the former is the depender while the latter is the dependee. **Readiness not only means that the dependee is started, but that it is ready to receive inputs**. + +Docker Compose lets you specify service dependencies using the `depends_on` attribute but this only ensures that the dependee is *started* before the depender [^depends_on], it has no notion of whether the dependee is ready to accept input. In Compose v2.1, you could depend on a `healthcheck` attribute to solve this problem, but this fix was removed in v3.0 - for reasons I disagree with.[^docker_startup_order] + +Nevertheless, Docker now recommends you write your own script that probes for when your dependee is ready before running the depender. This introduces some additional complexities like: instead of using stock images like *jaegertracing/jaeger-collector*, you'd need to create a custom image with an entrypoint script which probes *elasticsearch* for readiness before executing the *jaeger-collector* binary. + +> These kind of implementation gotchas can be a real pain, and why you might consider using a TaaS provider like mentioned earlier :) + +#### A word on trace sampling + +Although tracing clients are designed to have very little overhead, tracing every request may have significant impact on large production systems handling billions of requests. So, most solutions use a sampling strategy to choose what traces are recorded. For instance, the *jaeger-client* can be configured to record: all traces, a percentage of traces, or an adaptive number tailored to specific service requirements.[^jaeger_sampling] + +However, *Lightstep* promises 100% trace capturing (even for very large systems) with no significant overhead.[^lightstep_nosampling] I have no idea how they achieve this, but it would be very interesting to explore. + +#### Finally visualizing traces + +![](jaeger-query-ui.png) + +If the above setup was correctly done, you should be able to visit the *jaeger-query* UI endpoint and explore the traces stored in elasticsearch. The image above shows a trace of the `whereami-request` from the user, through all our services, and back. You can immediately infer that a bulk of the transaction time is spent in the `get-weather` request, meaning we can significantly improve our overall response time by optimizing this one request. Indeed you can do really sophisticated analyses like machine-learning-based anomaly detection, but that is beyond the scope of this article :) + +### Conclusion + +In this article, we examined the theory and practice of observing microservices by gathering and visualizing distributed tracing data. To do this we studied the concepts of Opentracing and implemented a tracing pipeline using the Jeager tracing backend. + +Consequently, we can note that implementing distributed tracing for microservices is nontrivial, especially if you're managing every component yourself—the increased complexity creates room for things to go wrong. So, I'd strongly encourage using a SaaS solution like *Lightstep* or *Elastic* to handle the complexities of trace collection, storage, and visualization. + +Nevertheless, our jaeger-based solution is sufficient for most use cases, and the implementation can further be simplified by using Kubernetes which has native support for liveness and readiness probes [^k8_liveness], hence making custom wait-scripts unncecessary. + +The complete project code is available on Github[^horus_repo], and I'll keep updating this article as I learn better ways of doing things. Please leave a comment if you have any questions or notice any errors. Hope you found it useful. + +## References + +[^ben1]: https://www.youtube.com/watch?v=EJV_CgiqlOE&list=PLwb6qBrEgFgOCOyAsVci18R0uWSqaP68g + +[^first_article]: https://hackernoon.com/monitoring-containerized-microservices-with-a-centralized-logging-architecture-ba6771c1971a + +[^opentracing1]: https://opentracing.io/ + +[^opentracing_spec]: https://opentracing.io/specification/ + +[^jaeger-client]: https://www.npmjs.com/package/jaeger-client + +[^opentracing-pkg]: https://www.npmjs.com/package/opentracing + +[^lightstep-pkg]: https://www.npmjs.com/package/lightstep-tracer + +[^elastic-pkg]: https://www.npmjs.com/package/elastic-apm-node-opentracing + +[^jaeger-agent]: https://www.jaegertracing.io/docs/1.11/architecture/#agent + +[^jaeger-collector]: https://www.jaegertracing.io/docs/1.11/architecture/#collector + +[^elasticsearch]: https://github.com/elastic/elasticsearch#elasticsearch + +[^jaeger-query]: https://www.jaegertracing.io/docs/1.11/architecture/#query + +[^jaeger-allinone]: https://hub.docker.com/r/jaegertracing/all-in-one + +[^depends_on]: https://docs.docker.com/compose/compose-file/#depends_on + +[^docker_startup_order]: https://docs.docker.com/compose/startup-order/ + +[^jaeger_sampling]: https://www.jaegertracing.io/docs/1.11/sampling/#client-sampling-configuration + +[^lightstep_nosampling]: https://lightstep.com/products/tracing/ + +[^k8_liveness]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/ + +[^horus_repo]: https://github.com/eyeezzi/horus \ No newline at end of file diff --git a/docs/distributed-tracing/client-instrumentation-structure.png b/docs/distributed-tracing/client-instrumentation-structure.png new file mode 100644 index 0000000..7958f00 Binary files /dev/null and b/docs/distributed-tracing/client-instrumentation-structure.png differ diff --git a/docs/distributed-tracing/client-instrumentation-structure.svg b/docs/distributed-tracing/client-instrumentation-structure.svg new file mode 100644 index 0000000..3c10bcc --- /dev/null +++ b/docs/distributed-tracing/client-instrumentation-structure.svg @@ -0,0 +1,2 @@ + +
Instrumenting a Nodejs App with Opentracing
Instrumenting a Nodejs App with Opentracing
Opentracing 1.0 API
Opentracing 1.0 API
opentracing
opentracing
jaeger-client
jaeger-client
elastic-apm-node-opentracing
elastic-apm-node-opentracing
lightstep-tracer
lightstep-tracer
Tracer
Tracer
Use in router middleware
Use in router middleware
Use in manual span creation and linking
Use in manual span creation and linking
Use in request interceptor
Use in request interceptor
\ No newline at end of file diff --git a/docs/distributed-tracing/jaeger-query-ui.png b/docs/distributed-tracing/jaeger-query-ui.png new file mode 100644 index 0000000..25cb98f Binary files /dev/null and b/docs/distributed-tracing/jaeger-query-ui.png differ diff --git a/docs/distributed-tracing/multiple-views-of-a-trace.png b/docs/distributed-tracing/multiple-views-of-a-trace.png new file mode 100644 index 0000000..2877e22 Binary files /dev/null and b/docs/distributed-tracing/multiple-views-of-a-trace.png differ diff --git a/docs/distributed-tracing/multiple-views-of-a-trace.svg b/docs/distributed-tracing/multiple-views-of-a-trace.svg new file mode 100644 index 0000000..af91e11 --- /dev/null +++ b/docs/distributed-tracing/multiple-views-of-a-trace.svg @@ -0,0 +1,2 @@ + +
Fig 2: A Transaction as an end-to-end request-response cycle
Fig 2: A Transaction as an end-to-end request-response cycle
Fig 3: A Trace as the record of a Transaction
Fig 3: A Trace as the record of a Transaction
Fig 1: High-level structure of services
Fig 1: High-level structure of services
api-server: GET /whereami
api-server: GET /whereami
api-server: GET /ip-service
api-server: GET /ip-service
api-server: GET /weather-service
api-server: GET /weather-service
0ms
0ms
50ms
50ms
200ms
200ms
user: GET /whereami
user: GET /whereami
Fig 4: A Trace is a DAG of Spans
Fig 4: A Trace is a DAG of Spans
childOf
childOf
childOf
childOf
childOf
childOf
user
whereami
[Not supported by viewer]
api-server
whereami
[Not supported by viewer]
api-server
GET /ip-service
[Not supported by viewer]
api-server
GET /weather-service
[Not supported by viewer]
User
User
api-service
api-service
ip-service
ip-service
weather-service
weather-service
api-service
api-service
User
User
IP
service
[Not supported by viewer]
weather
service
[Not supported by viewer]
\ No newline at end of file diff --git a/docs/distributed-tracing/tracing-backend.png b/docs/distributed-tracing/tracing-backend.png new file mode 100644 index 0000000..766d150 Binary files /dev/null and b/docs/distributed-tracing/tracing-backend.png differ diff --git a/docs/distributed-tracing/tracing-backend.svg b/docs/distributed-tracing/tracing-backend.svg new file mode 100644 index 0000000..fa91359 --- /dev/null +++ b/docs/distributed-tracing/tracing-backend.svg @@ -0,0 +1,2 @@ + +
jaeger-agent
jaeger-agent
jaeger-collector
jaeger-collector
storage-backend
storage-backend
elasticsearch
elasticsearch
Jaeger Query
and UI
[Not supported by viewer]
my-service
my-service
jaeger-client
jaeger-client
Developer
Developer
The Jaeger Tracing Backend
The Jaeger Tracing Backend
\ No newline at end of file diff --git a/docs/distributed-tracing/two-tracing-pipelines.png b/docs/distributed-tracing/two-tracing-pipelines.png new file mode 100644 index 0000000..9d7c7e7 Binary files /dev/null and b/docs/distributed-tracing/two-tracing-pipelines.png differ diff --git a/docs/distributed-tracing/two-tracing-pipelines.svg b/docs/distributed-tracing/two-tracing-pipelines.svg new file mode 100644 index 0000000..b6b0c77 --- /dev/null +++ b/docs/distributed-tracing/two-tracing-pipelines.svg @@ -0,0 +1,2 @@ + +
The 2 ways to setup a Tracing Pipeline
The 2 ways to setup a Tracing Pipeline
Tracing Agent
Tracing Agent
Tracing-as-a-Service
Tracing-as-a-Service
Database
Database
Query UI
Query UI
Developer
Developer
Tracing Client
Tracing Client
1
1
2
2
3
3
\ No newline at end of file diff --git a/jaeger-agent/Dockerfile b/jaeger-agent/Dockerfile new file mode 100644 index 0000000..44870f8 --- /dev/null +++ b/jaeger-agent/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.9.3 + +# required by wait script +RUN apk add --no-cache bash curl + +COPY jaeger-agent wait.sh /usr/local/bin/ + +RUN chmod +x /usr/local/bin/wait.sh \ No newline at end of file diff --git a/jaeger-agent/jaeger-agent b/jaeger-agent/jaeger-agent new file mode 100755 index 0000000..de67a9f Binary files /dev/null and b/jaeger-agent/jaeger-agent differ diff --git a/jaeger-agent/wait.sh b/jaeger-agent/wait.sh new file mode 100644 index 0000000..b952e08 --- /dev/null +++ b/jaeger-agent/wait.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Requires a container with Bash and Curl installed. + +is_healthy () { + retryInterval=${2:-5} + maxAttempts=${3:-1000000} + + i=0 + while [ $i -lt $maxAttempts ] + do + status=$(curl -s -L -o /dev/null -w %{http_code} $1) + + # stop trying if we get a success response + if [ $status -ge 200 ] && [ $status -lt 300 ] + then + return 0 + fi + + ((i++)) + echo "Attempt $i of $maxAttempts: Endpoint $1 returned code $status. Retrying in $retryInterval seconds." + sleep $retryInterval + done + + return 1 +} + +# args: endpoint, retryInterval (secs), maxAttempts, command +if is_healthy $1 $2 $3 +then + echo "Running command: $4" + # vulnerable to input-injection attacts (but I don't care :) + eval "$4" +else + echo "Exiting: Cannot run command because health-check failed." +fi \ No newline at end of file diff --git a/jaeger-collector/Dockerfile b/jaeger-collector/Dockerfile new file mode 100644 index 0000000..ffe57e9 --- /dev/null +++ b/jaeger-collector/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.9.3 + +# required by wait script +RUN apk add --no-cache bash curl + +COPY jaeger-collector wait.sh /usr/local/bin/ + +RUN chmod +x /usr/local/bin/wait.sh \ No newline at end of file diff --git a/jaeger-collector/jaeger-collector b/jaeger-collector/jaeger-collector new file mode 100755 index 0000000..df98da3 Binary files /dev/null and b/jaeger-collector/jaeger-collector differ diff --git a/jaeger-collector/wait.sh b/jaeger-collector/wait.sh new file mode 100644 index 0000000..b952e08 --- /dev/null +++ b/jaeger-collector/wait.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Requires a container with Bash and Curl installed. + +is_healthy () { + retryInterval=${2:-5} + maxAttempts=${3:-1000000} + + i=0 + while [ $i -lt $maxAttempts ] + do + status=$(curl -s -L -o /dev/null -w %{http_code} $1) + + # stop trying if we get a success response + if [ $status -ge 200 ] && [ $status -lt 300 ] + then + return 0 + fi + + ((i++)) + echo "Attempt $i of $maxAttempts: Endpoint $1 returned code $status. Retrying in $retryInterval seconds." + sleep $retryInterval + done + + return 1 +} + +# args: endpoint, retryInterval (secs), maxAttempts, command +if is_healthy $1 $2 $3 +then + echo "Running command: $4" + # vulnerable to input-injection attacts (but I don't care :) + eval "$4" +else + echo "Exiting: Cannot run command because health-check failed." +fi \ No newline at end of file diff --git a/jaeger-query/Dockerfile b/jaeger-query/Dockerfile new file mode 100644 index 0000000..ba9db54 --- /dev/null +++ b/jaeger-query/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.9.3 + +# required by wait script +RUN apk add --no-cache bash curl + +COPY jaeger-query wait.sh /usr/local/bin/ + +RUN chmod +x /usr/local/bin/wait.sh \ No newline at end of file diff --git a/jaeger-query/jaeger-query b/jaeger-query/jaeger-query new file mode 100755 index 0000000..1c77ccb Binary files /dev/null and b/jaeger-query/jaeger-query differ diff --git a/jaeger-query/wait.sh b/jaeger-query/wait.sh new file mode 100644 index 0000000..b952e08 --- /dev/null +++ b/jaeger-query/wait.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Requires a container with Bash and Curl installed. + +is_healthy () { + retryInterval=${2:-5} + maxAttempts=${3:-1000000} + + i=0 + while [ $i -lt $maxAttempts ] + do + status=$(curl -s -L -o /dev/null -w %{http_code} $1) + + # stop trying if we get a success response + if [ $status -ge 200 ] && [ $status -lt 300 ] + then + return 0 + fi + + ((i++)) + echo "Attempt $i of $maxAttempts: Endpoint $1 returned code $status. Retrying in $retryInterval seconds." + sleep $retryInterval + done + + return 1 +} + +# args: endpoint, retryInterval (secs), maxAttempts, command +if is_healthy $1 $2 $3 +then + echo "Running command: $4" + # vulnerable to input-injection attacts (but I don't care :) + eval "$4" +else + echo "Exiting: Cannot run command because health-check failed." +fi \ No newline at end of file diff --git a/user-simulator/.dockerignore b/user-simulator/.dockerignore index b44764a..c796e7b 100644 --- a/user-simulator/.dockerignore +++ b/user-simulator/.dockerignore @@ -1,4 +1,4 @@ .env -/node_modules +node_modules npm-debug.log .DS_Store \ No newline at end of file diff --git a/user-simulator/.gitignore b/user-simulator/.gitignore index b44764a..c796e7b 100644 --- a/user-simulator/.gitignore +++ b/user-simulator/.gitignore @@ -1,4 +1,4 @@ .env -/node_modules +node_modules npm-debug.log .DS_Store \ No newline at end of file diff --git a/user-simulator/Dockerfile b/user-simulator/Dockerfile index 21b4261..e1226e5 100644 --- a/user-simulator/Dockerfile +++ b/user-simulator/Dockerfile @@ -2,12 +2,17 @@ FROM node:10.10.0-alpine WORKDIR /usr/src/app +RUN apk add --no-cache bash curl \ + && npm install -g nodemon + +COPY wait.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/wait.sh + # optimization to only rebuild npm modules iff package[-lock].json changes COPY src/package*.json ./ - # For production `RUN npm install --only=production` RUN npm install - COPY src . -CMD ["npm", "start"] \ No newline at end of file + +CMD ["node", "."] \ No newline at end of file diff --git a/user-simulator/src/app.js b/user-simulator/src/app.js index 7cd387a..eab393f 100644 --- a/user-simulator/src/app.js +++ b/user-simulator/src/app.js @@ -1,20 +1,32 @@ -// const dotenv = require('dotenv') -// if (process.env.NODE_ENV !== 'production') { -// const result = dotenv.config() -// if (result.error) { -// throw result.error -// } -// } - const cron = require("node-cron") const axios = require('axios') +var opentracing = require('opentracing') +var initTracer = require('jaeger-client').initTracerFromEnv; +var tracer = initTracer() + cron.schedule(process.env.CRON_SCHEDULE, async () => { try { console.log(`Requesting token from API Server`) const token = await axios.get(`${process.env.API_SERVER_ADDRESS}/tokens`) console.log('Received token from server:', token.data) + + const span = tracer.startSpan('whereami-request') + + console.log(`Requesting location info from API Server`) + const location = await axios.get(`${process.env.API_SERVER_ADDRESS}/whereami`, { + headers: getCarrier(span, tracer) + }) + console.log('Received location from server:', location.data) + + span.finish() } catch (err) { console.error(`Token request failed with error: ${err.message}`) } }) + +function getCarrier(span, tracer) { + const carrier = {} + tracer.inject(span.context(), opentracing.FORMAT_HTTP_HEADERS, carrier) + return carrier +} \ No newline at end of file diff --git a/user-simulator/src/package-lock.json b/user-simulator/src/package-lock.json index 68c38ca..e4f27cd 100644 --- a/user-simulator/src/package-lock.json +++ b/user-simulator/src/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "ansi-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansi-color/-/ansi-color-0.2.1.tgz", + "integrity": "sha1-PnXAN0dSF1RO12Oo21cJ+prlv5o=" + }, "axios": { "version": "0.18.0", "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", @@ -13,57 +18,23 @@ "is-buffer": "^1.1.5" } }, - "connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=", - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", - "utils-merge": "1.0.1" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "bufrw": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bufrw/-/bufrw-1.2.1.tgz", + "integrity": "sha1-k/IiIptPX14s1VkjaJFAf5hTZjs=", "requires": { - "ms": "2.0.0" + "ansi-color": "^0.2.1", + "error": "^7.0.0", + "xtend": "^4.0.0" } }, - "dotenv": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.0.0.tgz", - "integrity": "sha512-FlWbnhgjtwD+uNLUGHbMykMOYQaTivdHEmYwAKFjn6GKe/CqY0fNae93ZHTd20snh9ZLr8mTzIL9m0APQ1pjQg==" - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "error": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", + "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.1", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", - "unpipe": "~1.0.0" + "string-template": "~0.2.1", + "xtend": "~4.0.0" } }, "follow-redirects": { @@ -89,6 +60,23 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, + "jaeger-client": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/jaeger-client/-/jaeger-client-3.15.0.tgz", + "integrity": "sha512-0SfuEE7E6XVLhu8th5JG/ACtnIWq4Tad0iSst3+De9HOMSz1RI0Tl1MLXzetudI670rqfCs4m37XCTMRgu8oxg==", + "requires": { + "node-int64": "^0.4.0", + "opentracing": "^0.13.0", + "thriftrw": "^3.5.0", + "uuid": "^3.2.1", + "xorshift": "^0.2.0" + } + }, + "long": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/long/-/long-2.4.0.tgz", + "integrity": "sha1-n6GAux2VAM3CnEFWdmoZleH0Uk8=" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -99,33 +87,45 @@ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-1.2.1.tgz", "integrity": "sha1-jJC8XccjpWKJsHhmVatKHEy2A2g=" }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "opentracing": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.13.0.tgz", + "integrity": "sha1-ajQUQvCdfYZrwR7QPeHjgo49aqs=" + }, + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" + }, + "thriftrw": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/thriftrw/-/thriftrw-3.11.3.tgz", + "integrity": "sha512-mnte80Go5MCfYyOQ9nk6SljaEicCXlwLchupHR+/zlx0MKzXwAiyt38CHjLZVvKtoyEzirasXuNYtkEjgghqCw==", "requires": { - "ee-first": "1.1.1" + "bufrw": "^1.2.1", + "error": "7.0.2", + "long": "^2.4.0" } }, - "parseurl": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", - "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" - }, - "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "xorshift": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xorshift/-/xorshift-0.2.1.tgz", + "integrity": "sha1-/NgiZ+k1HBPw+5xzMH8lMx0pxjo=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } } diff --git a/user-simulator/src/package.json b/user-simulator/src/package.json index e4a177c..db6b84e 100644 --- a/user-simulator/src/package.json +++ b/user-simulator/src/package.json @@ -11,7 +11,7 @@ "license": "ISC", "dependencies": { "axios": "^0.18.0", - "dotenv": "^6.0.0", - "node-cron": "^1.2.1" + "node-cron": "^1.2.1", + "jaeger-client": "^3.14.4" } } diff --git a/user-simulator/wait.sh b/user-simulator/wait.sh new file mode 100644 index 0000000..b952e08 --- /dev/null +++ b/user-simulator/wait.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Requires a container with Bash and Curl installed. + +is_healthy () { + retryInterval=${2:-5} + maxAttempts=${3:-1000000} + + i=0 + while [ $i -lt $maxAttempts ] + do + status=$(curl -s -L -o /dev/null -w %{http_code} $1) + + # stop trying if we get a success response + if [ $status -ge 200 ] && [ $status -lt 300 ] + then + return 0 + fi + + ((i++)) + echo "Attempt $i of $maxAttempts: Endpoint $1 returned code $status. Retrying in $retryInterval seconds." + sleep $retryInterval + done + + return 1 +} + +# args: endpoint, retryInterval (secs), maxAttempts, command +if is_healthy $1 $2 $3 +then + echo "Running command: $4" + # vulnerable to input-injection attacts (but I don't care :) + eval "$4" +else + echo "Exiting: Cannot run command because health-check failed." +fi \ No newline at end of file