Skip to content

Commit 34f65ef

Browse files
authored
Merge pull request eyeezzi#2 from eyeezzi/distributed-tracing
Distributed tracing
2 parents f0b0bb4 + 385964e commit 34f65ef

40 files changed

+1282
-507
lines changed

.vscode/launch.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "attach",
10+
"name": "Attach to user-simulator container",
11+
"address": "localhost",
12+
"port": 9230,
13+
"protocol": "inspector",
14+
"localRoot": "${workspaceFolder}/user-simulator/src",
15+
"remoteRoot": "/usr/src/app",
16+
"restart": true
17+
},
18+
{
19+
"type": "node",
20+
"request": "attach",
21+
"name": "Attach to api-server container",
22+
// should match the exposed debug address specified in docker-compose
23+
"address": "localhost",
24+
"port": 9229,
25+
"protocol": "inspector",
26+
// you typically copy the src folder into a location in docker...specify this mapping.
27+
"localRoot": "${workspaceFolder}/api-server/src",
28+
"remoteRoot": "/usr/src/app",
29+
"restart": true
30+
}
31+
]
32+
}

README.md

+69-18
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,94 @@
11
# Horus
22

3-
Monitoring containerized microservices with a centralized logging architecture.
3+
A project for learning about microservices observability.
44

5-
## Dependencies
5+
* Centralized Logging
6+
* Distributed Tracing
7+
8+
## Development Requirements
69

710
- Docker Compose v1.23.2
11+
- OSX or GNU/Linux
12+
13+
## Development Setup
14+
15+
1. Clone this repo and open the root folder in an IDE like *Visual Studio Code*.
16+
17+
2. For each microservice, rename `example.env` to `.env` and supply the needed secrets.
18+
> TODO: Eliminate this friction.
19+
20+
3. Start all microservices in *development mode*.
21+
22+
docker-compose -f docker-compose.dev.yml \
23+
up -d --build
24+
25+
> In Development Mode
26+
>
27+
> - You can attach a remote debugger to a running service Docker for seemless debugging like placing breakpoints and watching variables.
28+
> - Changes to source files will automatically restart the corresponding docker service.
29+
30+
4. Optionally, attach the IDE's debugger to a service as follows in Visual Studio Code: *shift+cmd+D > Select a debug configuration > F5*.
31+
> All vscode debug configurations are stored in *.vscode/launch.json*. You can modify configs as you see fit.
32+
33+
5. Visit http://localhost:16686 to view traces.
34+
35+
### Useful dev commands
836

9-
## Setup
37+
# list all running services
38+
docker-compose -f docker-compose.dev.yml ps
1039

11-
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.
40+
# stop all services
41+
docker-compose -f docker-compose.dev.yml down
1242

13-
2. Run the following commands.
43+
# restart all [or specific] service
44+
docker-compose -f docker-compose.dev.yml \
45+
up -d --no-deps --build [service-name]
1446

15-
docker-compose build --pull
16-
docker-compose up -d --force-recreate
17-
18-
3. Then log into your ELK SaaS and view your microservices logs.
47+
# tail logs from all [or specific] service
48+
docker-compose -f docker-compose.dev.yml \
49+
logs -f [service-name]
50+
51+
# see how an image was built
52+
docker history <image-name>
1953

20-
## Project Documentation
54+
## Project Architecture
2155

22-
### System Architecture
56+
### Logging Infrastructure
2357

2458
![](docs/container-architecture.svg)
2559

26-
I wrote an accompanying [article](https://hackernoon.com/monitoring-containerized-microservices-with-a-centralized-logging-architecture-ba6771c1971a) explaining this architecture.
60+
Read this [article](https://hackernoon.com/monitoring-containerized-microservices-with-a-centralized-logging-architecture-ba6771c1971a) for more details.
2761

28-
## Notes
62+
### Tracing Infrastructure
2963

30-
### Docker Networking
64+
![Tracing Backend Architecture](docs/distributed-tracing/tracing-backend.svg)
65+
66+
Read this [article](#todo) for more details.
67+
68+
## Miscellaneous Notes
69+
70+
### TODO (Improvement Considerations)
71+
72+
- Research **jaeger-operator**
3173

32-
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*.
74+
- 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?
75+
76+
- 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?
77+
78+
- Use sub-second precision for fluentd timestamps (probably best to use nanoseconds.)
3379

3480
### Best practices
3581

3682
1. You can pass secrets for a microservice using the `env_file` attribute in *docker-compose.yml*.
3783
2. Microservices can communicate using their service names if they are in the same docker network.
3884

39-
### Improvement Considerations
85+
### Docker Networking
86+
87+
By default each containerized process runs in an isolated network namespace. For inter-container communication, place them in the same network namespace.
4088

41-
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?
89+
### References
4290

43-
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?
91+
- https://medium.com/lucjuggery/docker-in-development-with-nodemon-d500366e74df
92+
- https://blog.risingstack.com/how-to-debug-a-node-js-app-in-a-docker-container/
93+
- https://codefresh.io/docker-tutorial/debug_node_in_docker/
94+
- https://code.visualstudio.com/docs/editor/debugging

api-server/.dockerignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.env
2-
/node_modules
3-
npm-debug.log
2+
node_modules
3+
yarn-error.log
44
.DS_Store

api-server/.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.env
2-
/node_modules
2+
node_modules
33
npm-debug.log
44
.DS_Store

api-server/Dockerfile

+12-5
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@ FROM node:10.10.0-alpine
22

33
WORKDIR /usr/src/app
44

5-
# optimization to only rebuild npm modules iff package[-lock].json changes
6-
COPY src/package*.json ./
5+
RUN apk add --no-cache bash curl \
6+
&& yarn global add nodemon
77

8-
# For production `RUN npm install --only=production`
9-
RUN npm install
8+
COPY wait.sh /usr/local/bin/
9+
RUN chmod +x /usr/local/bin/wait.sh
10+
11+
COPY src/package.json src/yarn.lock ./
12+
RUN yarn install
1013

1114
COPY src .
1215

13-
CMD ["npm", "start"]
16+
CMD ["node", "."]
17+
18+
# ref: https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md
19+
# https://nodejs.org/en/docs/guides/nodejs-docker-webapp/
20+
# https://runnable.com/blog/9-common-dockerfile-mistakes

api-server/README.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,16 @@
1313
docker logs -f <container-name>
1414

1515
# From another terminal on the host machine, test the server
16-
curl http://localhost:5000/api/v1/tokens
16+
curl http://localhost:5000/api/v1/tokens
17+
18+
## Dev Notes
19+
20+
To quickly test a container
21+
22+
docker build -t <image-name> --no-cache . \
23+
&& docker run -it -v $(pwd)/src:/usr/src/app <imgage-name> sh
24+
25+
docker build -t api-server --no-cache . \
26+
&& docker run -it -v $(pwd)/src:/usr/src/app api-server sh
27+
28+
> The `-v` flag maps host absolute path to container absolute path.

api-server/example.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
PORT=
1+
# TODO

api-server/src/app.js

+75-10
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,84 @@
1-
// const dotenv = require('dotenv')
2-
// if (process.env.NODE_ENV !== 'production') {
3-
// const result = dotenv.config()
4-
// if (result.error) {
5-
// throw result.error
6-
// }
7-
// }
8-
91
const app = require('express')()
102
const uuid = require('uuid/v1')
3+
const axios = require('axios')
4+
5+
// For portability, we initialize tracer from envars instead of local options.
6+
// See: https://www.npmjs.com/package/jaeger-client#environment-variables
7+
var opentracing = require('opentracing')
8+
var initTracer = require('jaeger-client').initTracerFromEnv;
9+
var tracer = initTracer()
10+
11+
app.use('/health', (req, res) => {
12+
res.json(null)
13+
})
1114

1215
app.use('/api/v1/tokens', (req, res) => {
16+
const span = tracer.startSpan('token-request')
17+
1318
console.log(`Handling request for token`)
1419
res.json({token: uuid()})
20+
21+
span.finish()
1522
})
1623

17-
const server = app.listen(process.env.PORT, () => {
24+
app.use('/api/v1/whereami', async (req, res, next) => {
25+
const parentSpan = createContinuationSpan(tracer, req, 'whereami-request')
26+
27+
try {
28+
// get location of IP Address
29+
const IP = '23.16.76.104'
30+
const locSpan = tracer.startSpan('get-location', {childOf: parentSpan})
31+
const location = await axios.get(`http://ip-api.com/json/${IP}`)
32+
locSpan.finish()
33+
const {lat, lon, city, country} = location.data
34+
35+
// do some other async task
36+
const fakeSpan = tracer.startSpan('get-weather', {childOf: parentSpan})
37+
const _ = await fakeFetch(1500, 0.7)
38+
fakeSpan.finish()
39+
40+
// return results
41+
const data = {lat: lat, lon: lon, city: city, country: country}
42+
res.json(data)
43+
} catch(err) {
44+
parentSpan.setTag('ERROR', err)
45+
next(err)
46+
}
47+
48+
parentSpan.finish()
49+
})
50+
51+
const server = app.listen(process.env.PORT || 3000, () => {
1852
console.log(`Listening on ${ server.address().address }:${ server.address().port }`)
19-
})
53+
})
54+
55+
function fakeFetch(msDelay, successRate) {
56+
return new Promise((resolve, reject) => {
57+
setTimeout(() => {
58+
if (Math.random() <= successRate) {
59+
resolve()
60+
} else {
61+
reject('Fake fetch failed randomly.')
62+
}
63+
}, msDelay)
64+
})
65+
}
66+
67+
function extractContext(tracer, req) {
68+
return tracer.extract(opentracing.FORMAT_HTTP_HEADERS, req.headers)
69+
}
70+
71+
// If the request is already being traced, continue the trace
72+
// else start a new trace.
73+
function createContinuationSpan(tracer, req, spanName) {
74+
const incomingSpanContext = extractContext(tracer, req)
75+
76+
let newSpan = null
77+
if (incomingSpanContext == null) {
78+
newSpan = tracer.startSpan(spanName)
79+
} else {
80+
newSpan = tracer.startSpan(spanName, {childOf: incomingSpanContext})
81+
}
82+
83+
return newSpan
84+
}

0 commit comments

Comments
 (0)