Base Project
The goal is to understand how it works inside an application to understand instrumentation. If you're DevOps and thinking: "this is the devs' problem", I can't blame you. Why didn't they do it in a language that can be automated? :D
The problem is that we need to create something to generate the traces we need.
β― tree
.
βββ docker #Dockerfiles
β βββ Dockerfile.auth
β βββ Dockerfile.todo
βββ docker-compose.yml
βββ package-lock.json
βββ package.json
βββ src
β βββ services
β βββ auth-service.ts # Auth code
β βββ todo-service.ts # TODOS code
βββ tsconfig.json
4 directories, 8 files
Analyzing docker-compose.yaml we have this. We're already mounting the volume inside each container. The volume is the project folder itself. That's why we don't need to do COPY . . later inside the dockerfiles.
The applications inside run on port 8080 each, so we'll put 8080 for auth and 8081 for todos to access them externally.
version: '3.8'
services:
todo:
build:
context: .
dockerfile: docker/Dockerfile.todo
volumes:
- .:/usr/src/app:rw
- /usr/src/app/node_modules
ports:
- 8081:8080
depends_on:
- redis
auth:
build:
context: .
dockerfile: docker/Dockerfile.auth
volumes:
- .:/usr/src/app:rw
- /usr/src/app/node_modules
ports:
- 8080:8080
redis:
image: redis:alpine
ports:
- '6380:6379'
# My machine uses redis personally so I'll use 6380 externally
One redis, one auth application and one todo. The dockerfile for both applications is the same, only changing the CMD that will start auth or todos.
| auth dockerfile | todo dockerfile |
|---|---|
| FROM node:22-alpine | FROM node:22-alpine |
| WORKDIR /usr/src/app | WORKDIR /usr/src/app |
| COPY package*.json ./ | COPY package*.json ./ |
| RUN npm install | RUN npm install |
| EXPOSE 8080 | EXPOSE 8080 |
| CMD ["npm", "run", "auth"] | CMD ["npm", "run", "todo"] |
Our project will be a simple NodeJS project that starts just by running docker-compose up to make you happy.
Now let's understand auth-service.js.
// Imports the Express.js framework to create web applications
import express from 'express';
// Creates a new Express application instance
const app = express();
// Defines a GET route for the '/auth' endpoint
app.get('/auth', (req, res) => {
// When accessed, returns a JSON object with a username
// req: contains HTTP request information
// res: used to send the response to the client
res.json({username: 'David Puziol'})
})
// Starts the server on port 8080
app.listen(8080, () => {
// Callback executed when the server is ready
// Prints message to console confirming it's running
console.log('Auth service is running on port 8080');
})
And we have the todo-service.js which will be our application. We're creating a web application that has a route to be called at /todos that makes a call to an auth:8080/auth service that we also defined and will be running in another container called auth and also populates redis.
// Imports the necessary packages
import express from 'express'; // Web framework
import axios from 'axios'; // HTTP client to make requests
import IORedis from 'ioredis'; // Redis client
// Creates an Express instance
const app = express();
// Configures connection with Redis using the 'redis' hostname (defined in docker-compose)
const redis = new IORedis({ host: 'redis' });
// Defines GET route /todos
app.get('/todos', async (req, res) => {
// Fetches user information from the authentication service
const user = await axios.get('http://auth:8080/auth');
// Fetches all keys in Redis that start with 'todo:'
const todoKeys = await redis.keys('todo:*');
// Array to store the todos
const todos: any[] = [];
// Iterates over each found key
for (const key of todoKeys) {
// Fetches the stored value for each key
const todoItem = await redis.get(key);
if (todoItem) {
// Converts the JSON string to object and adds to array
todos.push(JSON.parse(todoItem));
}
}
// Returns the todos and user information
res.json({ todos, user: user.data });
});
// Starts the server on port 8080
app.listen(8080, () => {
console.log('Todo service is running on port 8080');
});
// Function to initialize sample data in Redis
async function init() {
// Creates 4 initial todos using Promise.all for parallel execution
await Promise.all([
redis.set('todo:1', JSON.stringify({ name: 'Configure OpenTelemetry' })),
redis.set('todo:2', JSON.stringify({ name: 'Implement tracing' })),
redis.set('todo:3', JSON.stringify({ name: 'Add metrics' })),
redis.set('todo:4', JSON.stringify({ name: 'Configure exporters' }))
]);
}
// Executes the initialization function and handles possible errors
init().catch(console.error);
We haven't instrumented anything so far, ok? Just to be clear.
Let's bring up the entire environment?
# Either docker or podman works
podman compose up --build
And we have this here.
curl localhost:8080/auth
{"username":"David Prata"}
curl localhost:8081/todos
{"todos":[{"name":"Implement tracing"},{"name":"Configure exporters"},{"name":"Add metrics"},{"name":"Configure OpenTelemetry"}],"user":{"username":"David Prata"}}
The project is available in This Repository. In the main branch we have the complete project but what we've seen so far is the base branch and we'll grow along the journey.
git clone https://gitlab.com/davidpuziol/opentelemetry-project.git
git checkout base