In a microservices architecture, besides the advantages like scalability, faster time to market, and improving maintability, it also comes with disadvantages. One of the hardest problems with microservices is how to debug errors when they happen. With dozens to hundreds of microservices communicating with each other in an application, you can easily get lost in the ocean of logs where you don't know the root cause of the problem.
To quickly debug and find out the root cause of the problem, you need to apply distributed tracing for your application. By the end of the article, you will understand how to apply distributed tracing for Node.js services using Jaeger.
To follow along with the step-by-step, terminal-based instructions in article, you need to prepare the following prerequisites:
- Prepare an Ubuntu cloud server, preferably version 20.04 or above with 4 vCPUs and 16 GB Memory
- Install Docker Desktop latest version to deploy MongoDB for storing the data of the backend services and Jaeger to store traces of the API requests. Please note that you need Docker Compose with version 2 or above to run the Docker services in the article. Docker Compose V2 is included in latest version of Docker Desktop.
- Install
pm2
globally in the virtual machine to manage multiple backend services - Install OpenSSL to generate the RSA key to create the JWT key for authentication in the demo app.
The demo application is a blog application where users can make API requests to create a new blog or read existing blogs in the database. There are 3 types of users in the demo application.
- Anonymous users cannot read or create a new blog
- Authenticated users with
READ
permission can only read the blogs - Authenticated users with
WRITE
permission can read the blogs and create a new blog.
The blog application has 4 services:
- User service allows the administrator to create new users
- Authen service allows user to authenticate their credentials so that they can have the token to read or create new blog later on
- Author service checks the user token to see whether the user has permission to create a new blog or not
- Blog service allows users to read or create a new blog post
- Jwt issuer: api@example.net
- Jwt audience: api.blog.example.net
- Secret key: example
You can change the above values with your own values to build and run the application.
To build the demo application, you need to initialize the app project first.
-
Create a project directory from any location in your Vultr cloud server
$ mkdir nodejs-distributed-tracing-app
Navigate to the project directory
$ cd nodejs-distributed-tracing-app
-
Get the project directory path and save it. You will use this path to navigate to the project directory for convenience
$ pwd
Save the path to environment variable named
PROJECT_DIRECTORY
:$ export PROJECT_DIRECTORY=$(pwd)
-
Initialize the Node.js app with default options
$ npm init -y
-
Application dependencies
For the application to run, you need to install the dependencies for it so that it can interact with the Mongo database or sending logs to Jaeger. To do that, open the
package.json
file:$ nano package.json
Add the following content to the end of the file (remember to add a
,
character after thelicense
line to have a valid json file):"dependencies": { "@opentelemetry/auto-instrumentations-node": "^0.39.2", "@opentelemetry/exporter-trace-otlp-http": "^0.43.0", "@opentelemetry/resources": "^1.17.0", "@opentelemetry/sdk-node": "^0.43.0", "@opentelemetry/sdk-trace-base": "^1.17.0", "@opentelemetry/semantic-conventions": "^1.17.0", "auto-bind": "^1.1.0", "axios": "^1.5.1", "body-parser": "^1.17.1", "debug": "^2.6.6", "express": "latest", "express-api-cache": "^1.0.4", "express-validator": "^3.2.0", "extend-error": "0.0.2", "http-status-codes": "^1.1.6", "jsonwebtoken": "^7.4.0", "mongoose": "^6.1.1", "passport": "^0.3.2", "passport-jwt": "^2.2.1", "passport-local": "latest", "passport-strategy": "latest", "should": "^11.2.1", "validator": "^7.0.0" }
The above code lists all required dependencies to apply distributing tracing for Node.js services such as
passport
to manage authentication orexpress
to create the web server.Run the following command to install all dependencies:
$ npm install
-
Deploy Mongo database and Jaeger service
Create a new Docker Compose file to define the Mongo database and Jaeger service that you want to deploy:
nano docker-compose.yml
Add the following content to the
docker-compose.yml
file:version: "3.4" services: jaeger: container_name: jaeger image: jaegertracing/all-in-one:latest ports: - 16686:16686 - 14269:14269 - 4317:4317 - 4318:4318 - 14268:14268 mongodb: image: mongo ports: - '27017:27017' environment: MONGO_INITDB_DATABASE: tracing volumes: - db:/data/db volumes: db:
The above code defines new services named
jaeger
andmongodb
. These two services use the their latest Docker images from DockerHub and apply port mapping so that you can access them from outside their containers. For the MongoDB to have the persistent data, themongodb
service uses Docker volume with path mapping isdb:/data/db
.Run the following command to deploy the Mongo database and Jaeger service:
$ docker compose up -d
After you have initialized the project code, in this section, you create the code defining common configuration of your app.
-
Create a
config
directory from the root of the project$ mkdir config
Navigate to the
config
directory:$ cd config
-
Create a new file named
db.js
$ nano db.js
Add the following content to the
db.js
file then save it:module.exports = { MONGO_CONNECT_URL:"mongodb://localhost:27017/tracing?retryWrites=true&w=majority" };
The
db.js
file is for defining connection string so that your application can connect to the Mongo database. -
Create a new file named
global-paths.js
:$ nano global-paths.js
Add the following content to the
global-paths.js
then save it:global.APP_MODEL_PATH = APP_ROOT_PATH + 'model/'; global.APP_AUTH_STRATEGY = APP_ROOT_PATH + 'authstrategy/'; global.APP_CONTROLLER_PATH = APP_ROOT_PATH + 'controller/'; global.APP_HANDLER_PATH = APP_ROOT_PATH + 'handler/'; global.APP_SERVICE_PATH = APP_ROOT_PATH + 'service/'; global.APP_BASE_PACKAGE_PATH = APP_ROOT_PATH + 'base/'; global.APP_ERROR_PATH = APP_ROOT_PATH + 'error/'; global.APP_MANAGER_PATH = APP_ROOT_PATH + 'manager/'; global.APP_MIDDLEWARE_PATH = APP_ROOT_PATH + 'middleware/'; global.APP_ROUTE_PATH = APP_ROOT_PATH + 'route/'; global.CONFIG_BASE_PATH = __dirname + '/';
The
global-paths.js
file is for defining the variables that stores the directory path to the packages in your project. -
Create a new file named
index.js
:$ nano index.js
Add the following content to the
index.js
file and save it:module.exports = { db: require('./db'), server: require('./server'), jwtOptions: require('./jwt-options') };
The
index.js
file exports the object that contains the codes to import packages so that you can conveniently use it later on. -
Create a new file named
jwt-options.js
$ nano jwt-options.js
Add the following content to it:
module.exports = { issuer: "api@example.net", audience: "api.blog.example.net", algorithm: "RS256" };
-
Create a new file named
server.js
$ nano server.js
Add the following content to it:
module.exports = { PORT: process.env.PORT || 3000 };
The
server.js
file stores the configuration for the service port. Each service uses its own thePORT
environment variable. If the environment variable is not set, the service uses thePORT 3000
instead. -
Create a new directory named
secret
inside theconfig
directory$ mkdir secret
Navigate to the
secret
directory:$ cd secret
The
secret
directory stores the SSH private key, and the SSH public key to generate a secured JWT token.Run the following command to generate an RSA private key file without passphrase:
$ openssl genrsa -out jwt-key.pem 2048
Generate the public key file by running the following command:
$ openssl rsa -in jwt-key.pem -pubout -outform PEM -out jwt-key.pub
You also need a
secret
key for authentication to create every new user.Create a new file named
secret.key
:nano secret.key
Add the following content to the
secret.key
file. You should not have any space or new line in the file. Otherwise the secret key will not be valid when you use it in the authorization header in the APIs.example
The value of the
secret
key for creating new user isexample
. You will use this value when makingcurl
request to send a new request to the API creating new user. -
Create a new file named
ecosystem.config.js
Navigate to the root project:
cd $PROJECT_DIRECTORY
Create a new file named
ecosystem.config.js
:nano ecosystem.config.js
Add the following content to it:
module.exports = { apps : [ { name: 'authenSvc', script: 'authenSvc.js', instances: 1, autorestart: true, watch: true, max_memory_restart: '512M', env: { SERVICE_NAME: 'authenSvc', NODE_ENV: 'development', PORT: 3001 } }, { name: 'userSvc', script: 'userSvc.js', instances: 1, autorestart: true, watch: true, max_memory_restart: '512M', env: { SERVICE_NAME: 'userSvc', NODE_ENV: 'development', PORT: 3002 } }, { name: 'blogSvc', script: 'blogSvc.js', instances: 1, autorestart: true, watch: true, max_memory_restart: '512M', env: { SERVICE_NAME: 'blogSvc', NODE_ENV: 'development', PORT: 3003 } }, { name: 'authorSvc', script: 'authorSvc.js', instances: 1, autorestart: true, watch: true, max_memory_restart: '512M', env: { SERVICE_NAME: 'authorSvc', NODE_ENV: 'development', PORT: 3004 } } ] };
The
ecosystem.config.js
defines the configuration such asPORT
number ormax_memory_restart
for the authentication, authorization, user management, and blog services. Thepm2
tool uses this information to deploy the services.
To insert data into the MongoDB database, you need to create the data model objects to interact with the db.
-
Create a new
app
directory$ mkdir app
-
The
model
directoryNavigate to the
app
directory$ cd app
Create the
model
directory$ mkdir model
-
Create a
post.js
file inside themodel
directoryNavigate to the
model
directory$ cd model
Run the following command to create the
post.js
file:$ nano post.js
Add the following content to the
post.js
file:const mongoose = require('mongoose'); mongoose.set("strictQuery", true); const Schema = mongoose.Schema; const ObjectId = Schema.ObjectId; let BlogPostSchema = new Schema({ title: String, content: String, authorId: { type: ObjectId, required: true }, dateCreated: {type: Date, default: Date.now}, dateModified: {type: Date, default: Date.now}, }); BlogPostSchema.pre('update', function (next, done) { this.dateModified = Date.now(); next(); }); BlogPostSchema.pre('save', function (next, done) { this.dateModified = Date.now(); next(); }); BlogPostSchema.methods.toJSON = function () { let obj = this.toObject(); delete obj.__v; return obj }; module.exports.BlogPostModel = mongoose.model('Post', BlogPostSchema);
The
post.js
file defines the data model to insert and update blog posts into the MongoDB database. -
Create a
user.js
file$ nano user.js
Add the following content to the
user.js
file:let mongoose = require('mongoose'); let Schema = mongoose.Schema; const crypto = require('crypto'); let UserSchema = new Schema({ firstName: String, lastName: String, salt: { type: String, required: true }, isActive: {type: Boolean, default: true}, dateCreated: {type: Date, default: Date.now}, role:{type:String, required: true}, email: String, hashedPassword: { type: String, required: true, }, }); UserSchema.methods.toJSON = function () { let obj = this.toObject(); delete obj.hashedPassword; delete obj.__v; delete obj.salt; return obj }; UserSchema.virtual('id') .get(function () { return this._id; }); UserSchema.virtual('password') .set(function (password) { this.salt = crypto.randomBytes(32).toString('base64'); this.hashedPassword = this.encryptPassword(password, this.salt); }) .get(function () { return this.hashedPassword; }); UserSchema.methods.encryptPassword = function (password, salt) { return crypto.createHmac('sha1', salt).update(password).digest('hex'); }; UserSchema.methods.checkPassword = function (password) { return this.encryptPassword(password, this.salt) === this.hashedPassword; }; module.exports.UserModel = mongoose.model('User', UserSchema);
The
user.js
file defines the data model to create and retrieve user data from the MongoDB database. -
The
auth
directoryCreate the
auth
directory$ mkdir auth
Navigate to it
$ cd auth
The
auth
directory creates the data model for creating and revoking the JWT token.Run the following command to create a new
jwt-token.js
file inside theauth
directory:$ nano jwt-token.js
Add the following content to the
jwt-token.js
file:class JwtToken { constructor(token) { this.token = token; } } module.exports = JwtToken;
The above code defines the
JwtToken
class to initialize the Jwt token.Run the following command to create a new file named
revoked-token.js
inside theauth
directory:$ nano revoked-token.js
Add the following content to the
revoked-token.js
file:let mongoose = require('mongoose'); let Schema = mongoose.Schema; let RevokedTokenScheme = new Schema({ token: String, date: {type: Date, default: Date.now} }); module.exports.RevokedTokenModel = mongoose.model('RevokedToken', RevokedTokenScheme);
The above code defines the
RevokedTokenSchema
model to revoke the token when the token is expired.
In this section, you create the manager functions for authenticating, creating API responses, and validating API format.
-
Create a
auth.js
fileNavigate to the
app
directory$ cd $PROJECT_DIRECTORY/app
Create a new directory named
manager
$ mkdir manager
Navigate to the
manager
directory:$ cd manager
Run the following command to create a new file named
auth.js
:$ nano auth.js
Add the following content to it:
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind'); const JwtRsStrategy = require(APP_AUTH_STRATEGY + 'jwt-rs'); const SecretKeyAuth = require(APP_AUTH_STRATEGY + 'secret-key'); const CredentialsAuth = require(APP_AUTH_STRATEGY + 'credentials'); const ExtractJwt = require("passport-jwt").ExtractJwt; const JwtToken = require(APP_MODEL_PATH + 'auth/jwt-token'); const RevokedToken = require(APP_MODEL_PATH + 'auth/revoked-token').RevokedTokenModel; const ForbiddenError = require(APP_ERROR_PATH + 'forbidden'); class AuthManager extends BaseAutoBindedClass { constructor() { super(); this._passport = require('passport'); this._strategies = []; this._jwtTokenHandler = require('jsonwebtoken'); this._setupStrategies(); this._setPassportStrategies(); } _setupStrategies() { // Init JWT strategy let jwtOptions = this._provideJwtOptions(); let secretKeyAuth = new SecretKeyAuth({secretKey: this._provideSecretKey()}); let jwtRs = new JwtRsStrategy(jwtOptions, this._verifyRevokedToken); this._strategies.push(jwtRs); this._strategies.push(new CredentialsAuth()); this._strategies.push(secretKeyAuth); } _verifyRevokedToken(token, payload, callback) { RevokedToken.find({token: token}, function (err, docs) { if (docs.length) { callback.onFailure(new ForbiddenError("Token has been revoked")); } else { callback.onVerified(token, payload); } }); } extractJwtToken(req) { return ExtractJwt.fromAuthHeader()(req); } _provideJwtOptions() { let config = global.config; let jwtOptions = {}; jwtOptions.extractJwtToken = ExtractJwt.fromAuthHeader(); jwtOptions.privateKey = this._provideJwtPrivateKey(); jwtOptions.publicKey = this._provideJwtPublicKey(); jwtOptions.issuer = config.jwtOptions.issuer; jwtOptions.audience = config.jwtOptions.audience; return jwtOptions; } _provideJwtPublicKey() { const fs = require('fs'); return fs.readFileSync(CONFIG_BASE_PATH + 'secret/jwt-key.pub', 'utf8'); } _provideJwtPrivateKey() { const fs = require('fs'); return fs.readFileSync(CONFIG_BASE_PATH + 'secret/jwt-key.pem', 'utf8'); } _provideSecretKey() { const fs = require('fs'); return fs.readFileSync(CONFIG_BASE_PATH + 'secret/secret.key', 'utf8'); } providePassport() { return this._passport; } getSecretKeyForStrategy(name) { for (let i = 0; i < this._strategies.length; i++) { let strategy = this._strategies[i]; if (strategy && strategy.name === name) { return strategy.provideSecretKey(); } } } _setPassportStrategies() { let passport = this._passport; this._strategies.forEach(function (strategy) { passport.use(strategy); }); } signToken(strategyName, payload, options) { let key = this.getSecretKeyForStrategy(strategyName); switch (strategyName) { case 'jwt-rs-auth': return new JwtToken( this._jwtTokenHandler.sign(payload, key, options) ); default: throw new TypeError("Cannot sign toke for the " + strategyName + " strategy"); } } } exports = module.exports = new AuthManager();
The
auth.js
file gives basic functions for authenticating tasks such asextractJwtToken
orgetSecretKeyForStrategy
. -
Create a new file named
response.js
$ nano response.js
Add the following content to it:
const HttpStatus = require('http-status-codes'); const BasicResponse = { "success": false, "message": "", "data": {} }; class ResponseManager { constructor() { } static get HTTP_STATUS() { return HttpStatus; } static getDefaultResponseHandler(res) { return { onSuccess: function (data, message, code) { ResponseManager.respondWithSuccess(res, code || ResponseManager.HTTP_STATUS.OK, data, message); }, onError: function (error) { ResponseManager.respondWithError(res, error.status || 500, error.message || 'Unknown error'); } }; } static getDefaultResponseHandlerData(res) { return { onSuccess: function (data, message, code) { ResponseManager.respondWithSuccess(res, code || ResponseManager.HTTP_STATUS.OK, data, message); }, onError: function (error) { ResponseManager.respondWithErrorData(res, error.status, error.message, error.data); } }; } static getDefaultResponseHandlerError(res, successCallback) { return { onSuccess: function (data, message, code) { successCallback(data, message, code); }, onError: function (error) { ResponseManager.respondWithError(res, error.status || 500, error.message || 'Unknown error'); } }; } static getDefaultResponseHandlerSuccess(res, errorCallback) { return { onSuccess: function (data, message, code) { ResponseManager.respondWithSuccess(res, code || ResponseManager.HTTP_STATUS.OK, data, message); }, onError: function (error) { errorCallback(error); } }; } static generateHATEOASLink(link, method, rel) { return { link: link, method: method, rel: rel } } static respondWithSuccess(res, code, data, message = "", links = []) { let response = Object.assign({}, BasicResponse); response.success = true; response.message = message; response.data = data; response.links = links; res.status(code).json(response); } static respondWithErrorData(res, errorCode, message = "", data = "", links = []) { let response = Object.assign({}, BasicResponse); response.success = false; response.message = message; response.data = data; response.links = links; res.status(errorCode).json(response); } static respondWithError(res, errorCode, message = "", links = []) { let response = Object.assign({}, BasicResponse); response.success = false; response.message = message; response.links = links; res.status(errorCode).json(response); } } module.exports = ResponseManager;
The
response.js
file creates functions to set up default handling responses for the APIs such asrespondWithSuccess
orresponseWithErrorData
. -
Create a new file named
validation.js
$ nano validation.js
Add the following content to it:
const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind'); const expressValidator = require('express-validator'); class ValidationManager extends BaseAutoBindedClass { constructor() { super(); } provideDefaultValidator() { return expressValidator({ errorFormatter: ValidationManager.errorFormatter }) } static errorFormatter(param, msg, value) { let namespace = param.split('.'), root = namespace.shift(), formParam = root; while (namespace.length) { formParam += '[' + namespace.shift() + ']'; } return { param: formParam, msg: msg, value: value }; } } module.exports = ValidationManager;
The
validation.js
file creates functions that handle default validation for the API.
When working with APIs, error handling mechanism helps to give meaningful message to the clients so that they can understand the reason why the API request is failed. In this section, you create error handling functions to do that.
-
The
err
directoryNavigate to the
app
directory$ cd ..
Create the
err
directory$ mkdir error
The
error
directory stores the error handling files. -
Create a new file named
already-exists.js
Navigate to the
error
directory$ cd error
Create the
already-exists.js
file$ nano already-exists.js
Add the following content to the
already-exists.js
file then save it:'use strict'; const BaseError = require(APP_ERROR_PATH + 'base'); class AlreadyExistsError extends BaseError { constructor(message) { super(message, 409); } } module.exports = AlreadyExistsError;
The
already-exist.js
file creates an error message and status code when the clients create a new user with the existing email. -
Create a new file named
base.js
$ nano base.js
Add the following content to the
base.js
file then save it:'use strict'; class BaseError extends Error { constructor(message, status) { // Calling parent constructor of base Error class. super(message); // Capturing stack trace, excluding constructor call from it. Error.captureStackTrace(this, this.constructor); // Saving class getName in the property of our custom error as a shortcut. this.name = this.constructor.name; // You can use any additional properties you want. // I'm going to use preferred HTTP status for this error types. // `500` is the default value if not specified. this.status = status || 500; } } module.exports = BaseError;
The
base.js
file creates the default API response with unknown error. -
Create a new file named
forbidden.js
$ nano forbidden.js
Add the following content to the
forbiden.js
file then save it:'use strict'; const BaseError = require(APP_ERROR_PATH + 'base'); class ForbiddenError extends BaseError { constructor(message) { super(message, 403); } } module.exports = ForbiddenError;
The
forbidden.js
file helps to show foribidden error when the clients do not have permission to do certain tasks. -
Create a new file named
invalid-payload.js
$ nano invalid-payload.js
Add the following content to the
invalid-payload.js
file then save it:'use strict'; const BaseError = require(APP_ERROR_PATH + 'base'); class InvalidPayloadError extends BaseError { constructor(message) { super(message, 400); } } module.exports = InvalidPayloadError;
The
invalid-payload.js
file shows an error to the clients when they send the payload that is invalid. -
Create a new file named
not-found.js
$ nano not-found.js
Add the following content to the
not-found.js
file then save it:'use strict'; const BaseError = require(APP_ERROR_PATH + 'base'); class NotFoundError extends BaseError { constructor(message) { super(message, 404); } } module.exports = NotFoundError;
The
not-found.js
file shows error to the clients when the URL path they input does not exist. -
Create a new file named
unauthorized.js
$ nano unauthorized.js
Add the following content to the
unauthorized.js
then save it:'use strict'; const BaseError = require(APP_ERROR_PATH + 'base'); class UnauthorizedError extends BaseError { constructor(message) { super(message, 401); } } module.exports = UnauthorizedError;
The
unauthorized.js
file shows an error to the clients when the token is invalid. -
Create a new file named
validation.js
$ nano validation.js
Add the following content to the
validation.js
file then save it:'use strict'; const BaseError = require(APP_ERROR_PATH + 'base'); class ValidationError extends BaseError { constructor(message) { super(message, 400); } } module.exports = ValidationError;
The
validation.js
file checks if there is any invalid information in the API request.
-
Create a new directory called
base
inside theapp
directoryNavigate to the
app
directory$ cd ..
Create the
base
directory$ mkdir base
-
Create a new file named
base-autobind.js
Navigate to the
base
directory$ cd base
Create the
base-autobind.js
file$ nano base-autobind.js
Add the following content to it:
const autoBind = require('auto-bind'); class BaseAutoBindedClass { constructor() { autoBind(this); } } module.exports = BaseAutoBindedClass;
The
base-autobind.js
class creates auto binding class that other classes can inherit from it. Auto-binding allows you to create a new function using existing function or change the function context.
The OpenTelemetry
library gives the SDK that supports automatically tracing for Node.js services. To trace which requests have been made inside an API request, you need to register the instrumentation type for the requests.
For example, you can trace for the HTTP requests, Express requests, and Mongo database requests by creating the NodeSDK
variable instance with definition for instrumentations
as [getNodeAutoInstrumentations()]
like the one below:
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: `${process.env.SERVICE_NAME}`,
}),
instrumentations: [ getNodeAutoInstrumentations()],
spanProcessor: new BatchSpanProcessor(traceExporter),
});
-
Navigate to the root directory of the project
$ cd $PROJECT_DIRECTORY
-
Create a new file named
tracing.js
$ nano tracing.js
-
Add the following content to the
tracing.js
fileconst { OTLPTraceExporter } = require( '@opentelemetry/exporter-trace-otlp-http'); const { Resource } = require('@opentelemetry/resources'); const { BatchSpanProcessor } = require( '@opentelemetry/sdk-trace-base'); const { NodeSDK } = require('@opentelemetry/sdk-node'); const { SemanticResourceAttributes } = require ('@opentelemetry/semantic-conventions'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const traceExporter = new OTLPTraceExporter({ url: 'http://localhost:4318/v1/traces', }); const sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: `${process.env.SERVICE_NAME}`, }), instrumentations: [ getNodeAutoInstrumentations()], spanProcessor: new BatchSpanProcessor(traceExporter), }); (async () => { try { await sdk.start(); console.log('Tracing started.'); } catch (error) { console.error(error); } })(); // For local development to stop the tracing using Control+c process.on('SIGINT', async () => { try { await sdk.shutdown(); console.log('Tracing finished.'); } catch (error) { console.error(error); } finally { process.exit(0); } });
In the above code, you tell
opentelemetry
to trace HTTP requests, Express requests and MongoDB requests and send the traces to the Jaeger backend service.
The Authen method creates methods to apply authentication mechanisms for the users.
-
The
authstrategy
directoryNavigate to the
app
directory$ cd app
Create the
authstrategy
directory$ mkdir authstrategy
-
Navigate to the
authstrategy
directory$ cd authstrategy
-
Create a new file named
base-auth.js
$ nano base-auth.js
As the name suggests, the
base-auth.js
creates base methods for applying authentication.Add the following content to the
base-auth.js
file and save it:const BasePassportStrategy = require('passport-strategy'); class BaseAuthStrategy extends BasePassportStrategy { constructor() { super(); } _initStrategy() { throw new Error("Not Implemented"); } authenticate(req) { throw new Error("Not Implemented"); } authenticate(req, options) { throw new Error("Not Implemented"); } get name() { throw new Error("Not Implemented"); } provideOptions() { throw new Error("Not Implemented"); } provideSecretKey() { throw new Error("Not Implemented"); } } exports = module.exports = BaseAuthStrategy;
The above code defines prototypes for methods such as
provideSecretKey
orprovideOptions
. -
Create a new file named
credentials.js
$ nano credentials.js
Add the following content to the
credentials.js
file then save it:const LocalAuthStrategy = require('passport-local').Strategy; const UserModel = require(APP_MODEL_PATH + 'user').UserModel; const UnauthorizedError = require(APP_ERROR_PATH + 'unauthorized'); const NotFoundError = require(APP_ERROR_PATH + 'not-found'); class CredentialsAuthStrategy extends LocalAuthStrategy { constructor() { super(CredentialsAuthStrategy.provideOptions(), CredentialsAuthStrategy.handleUserAuth); } get name() { return 'credentials-auth'; } static handleUserAuth(username, password, done) { UserModel.findOne({email: username}, function (err, user) { if (err) { return done(err); } if (!user) { return done(new NotFoundError("User not found"), false); } if (!user.checkPassword(password)) { return done(new UnauthorizedError("Invalid credentials"), false); } return done(null, user); }); } static provideOptions() { return { usernameField: 'email', passReqToCallback: false, passwordField: 'password', session: false }; } getSecretKey() { throw new Error("No key is required for this type of auth"); } } exports = module.exports = CredentialsAuthStrategy;
The
credentials.js
defines methods that validate the user credentials. -
Create a new file named
jwt-rs.js
$ nano jwt-rs.js
Add the following content to the
jwt-rs.js
file then save it:const passport = require('passport-strategy') , jwt = require('jsonwebtoken'); const BaseAuthStrategy = require(APP_AUTH_STRATEGY + 'base-auth'); class JwtRsStrategy extends BaseAuthStrategy { constructor(options, verify) { super(); this._options = options; this._customVerifier = verify; this._initStrategy(); } _initStrategy() { passport.Strategy.call(this); let options = this.provideOptions(); if (!options) { throw new TypeError('JwtRsStrategy requires options'); } this._privateKey = options.privateKey; if (!this._privateKey) { throw new TypeError('JwtRsStrategy requires a private key'); } this._publicKey = options.publicKey; if (!this._publicKey) { throw new TypeError('JwtRsStrategy requires a public key'); } this._extractJwtToken = options.extractJwtToken; if (!this._extractJwtToken) { throw new TypeError('JwtRsStrategy requires a function to parse jwt from requests'); } this._verifyOpts = {}; if (options.issuer) { this._verifyOpts.issuer = options.issuer; } if (options.audience) { this._verifyOpts.audience = options.audience; } if (options.algorithms) { this._verifyOpts.algorithms = options.algorithms; } if (options.ignoreExpiration != null) { this._verifyOpts.ignoreExpiration = options.ignoreExpiration; } } get name() { return 'jwt-rs-auth'; } provideSecretKey() { return this._privateKey; } authenticate(req, callback) { let self = this; let token = self._extractJwtToken(req); if (!token) { return callback.onFailure(new Error("No auth token provided")); } // Verify the JWT JwtRsStrategy._verifyDefault(token, this._publicKey, this._verifyOpts, function (jwt_err, payload) { if (jwt_err) { return callback.onFailure(jwt_err); } else { try { // If custom verifier was set delegate the flow control if (self._customVerifier) { self._customVerifier(token, payload, callback); } else { callback.onVerified(token, payload); } } catch (ex) { callback.onFailure(ex); } } }); } provideOptions() { return this._options; } static _verifyDefault(token, publicKey, options, callback) { return jwt.verify(token, publicKey, options, callback); } } module.exports = JwtRsStrategy;
The
jwt-rs.js
creates methods that verify the user provided token. -
Create a new file named
secret-key.js
$ nano secret-key.js
Add the following content to the
secret-key.js
file then save it:const BaseAuthStrategy = require(APP_AUTH_STRATEGY + 'base-auth'); const InvalidPayloadError = require(APP_ERROR_PATH + 'invalid-payload'); const UnauthorizedError = require(APP_ERROR_PATH + 'unauthorized'); class SecretKeyAuthStrategy extends BaseAuthStrategy { constructor(options) { super(); this._options = options; this._initStrategy(); } static get AUTH_HEADER() { return "Authorization"; } _initStrategy() { } get name() { return 'secret-key-auth'; } static _extractKeyFromHeader(req) { return req.headers[SecretKeyAuthStrategy.AUTH_HEADER.toLowerCase()]; } _verifyCredentials(key) { return key === this.provideSecretKey(); } authenticate(req, callback) { let secretKey = SecretKeyAuthStrategy._extractKeyFromHeader(req); if (!secretKey) { return callback.onFailure(new InvalidPayloadError("No auth key provided")); } if (this._verifyCredentials(secretKey)) { return callback.onVerified(); } else { return callback.onFailure(new UnauthorizedError("Invalid secret key")); } } provideSecretKey() { return this._options.secretKey; } provideOptions() { return this._options; } } module.exports = SecretKeyAuthStrategy;
The
secret-key.js
file creates methods to verify the secret key of the user.
In this section, you create the code that implements the handler methods
-
The
handler
directoryNavigate to the
app
directory$ cd ..
Create the
handler
directory$ mkdir handler
The
handler
directory stores the functions that handle the business logic of the application service. -
Navigate to the
handler
directory$ cd handler
-
Create a new file named
user.js
$ nano user.js
The
user.js
file defines thecreateNewUser
method which will be used by the API for creating new users. -
Add the following content to the
user.js
then save itconst UserModel = require(APP_MODEL_PATH + 'user').UserModel; const AlreadyExistsError = require(APP_ERROR_PATH + 'already-exists'); const ValidationError = require(APP_ERROR_PATH + 'validation'); class UserHandler { constructor() { this._validator = require('validator'); } static get USER_VALIDATION_SCHEME() { return { 'firstName': { notEmpty: true, isLength: { options: [{min: 2, max: 15}], errorMessage: 'First getName must be between 2 and 15 chars long' }, errorMessage: 'Invalid First Name' }, 'lastName': { notEmpty: true, isLength: { options: [{min: 2, max: 15}], errorMessage: 'Lastname must be between 2 and 15 chars long' }, errorMessage: 'Invalid First Name' }, 'email': { isEmail: { errorMessage: 'Invalid Email' }, errorMessage: "Invalid email provided" }, 'password': { notEmpty: true, isLength: { options: [{min: 6, max: 35}], errorMessage: 'Password must be between 6 and 35 chars long' }, errorMessage: 'Invalid Password Format' } }; } createNewUser(req, callback) { let data = req.body; let validator = this._validator; req.checkBody(UserHandler.USER_VALIDATION_SCHEME); req.getValidationResult() .then(function (result) { if (!result.isEmpty()) { let errorMessages = result.array().map(function (elem) { return elem.msg; }); throw new ValidationError('There are validation errors: ' + errorMessages.join(' && ')); } return new UserModel({ firstName: validator.trim(data.firstName), lastName: validator.trim(data.lastName), email: validator.trim(data.email), password: validator.trim(data.password), role:validator.trim(data.role) }); }) .then((user) => { return new Promise(function (resolve, reject) { UserModel.find({email: user.email}, function (err, docs) { if (docs.length) { reject(new AlreadyExistsError("User already exists")); } else { resolve(user); } }); }); }) .then((user) => { user.save(); return user; }) .then((saved) => { callback.onSuccess(saved); }) .catch((error) => { callback.onError(error); }); } } module.exports = UserHandler;
In the above code, you implement logic for creating new user, such as validating the user email, the user firstName or lastName or checking if the user is existed or not.
-
Create a new file named
post.js
$ nano post.js
The
post.js
file creates methods to create new blog posts and get all blogs.Add the following content to the
post.js
file then save it:const BlogPostModel = require(APP_MODEL_PATH + 'post').BlogPostModel; const ValidationError = require(APP_ERROR_PATH + 'validation'); const NotFoundError = require(APP_ERROR_PATH + 'not-found'); const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind'); class BlogPostHandler extends BaseAutoBindedClass { constructor() { super(); this._validator = require('validator'); } static get BLOG_POST_VALIDATION_SCHEME() { return { 'title': { notEmpty: true, isLength: { options: [{min: 10, max: 150}], errorMessage: 'Post title must be between 2 and 150 chars long' }, errorMessage: 'Invalid post title' }, 'content': { notEmpty: true, isLength: { options: [{min: 50, max: 3000}], errorMessage: 'Post content must be between 150 and 3000 chars long' }, errorMessage: 'Invalid post content' }, 'authorId': { isMongoId: { errorMessage: 'Invalid Author Id' }, errorMessage: "Invalid email provided" } }; } createNewPost(req, callback) { let data = req.body; let validator = this._validator; req.checkBody(BlogPostHandler.BLOG_POST_VALIDATION_SCHEME); req.getValidationResult() .then(function (result) { if (!result.isEmpty()) { let errorMessages = result.array().map(function (elem) { return elem.msg; }); throw new ValidationError('There are validation errors: ' + errorMessages.join(' && ')); } return new BlogPostModel({ title: validator.trim(data.title), content: validator.trim(data.content), authorId: data.authorId, }); }) .then((user) => { user.save(); return user; }) .then((saved) => { callback.onSuccess(saved); }) .catch((error) => { callback.onError(error); }); } getAllPosts(req, callback) { let data = req.body; new Promise(function (resolve, reject) { BlogPostModel.find({}, function (err, posts) { if (err !== null) { reject(err); } else { resolve(posts); } }); }) .then((posts) => { callback.onSuccess(posts); }) .catch((error) => { callback.onError(error); }); } } module.exports = BlogPostHandler;
In the above code, you implement the logic to check the validity of the post before saving it to the MongoDB database and retrieving all the posts when user request it.
-
Create a new file named
auth.js
$ nano auth.js
Add the following content to the
auth.js
file then save it:const RevokedToken = require(APP_MODEL_PATH + 'auth/revoked-token').RevokedTokenModel; const NotFoundError = require(APP_ERROR_PATH + 'invalid-payload'); const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind'); let crypto = require('crypto'); const SHA_HASH_LENGTH = 64; const ForbiddenError = require(APP_ERROR_PATH + 'forbidden'); class AuthHandler extends BaseAutoBindedClass { constructor() { super(); this._jwtTokenHandler = require('jsonwebtoken'); this._authManager = require(APP_MANAGER_PATH + 'auth'); } issueNewToken(req, user, callback) { let that = this; if (user) { let userToken = that._authManager.signToken("jwt-rs-auth", that._provideTokenPayload(user), that._provideTokenOptions()); callback.onSuccess(userToken); } else { callback.onError(new NotFoundError("User not found")); } } _hashToken(token) { return crypto.createHash('sha256').update(token).digest('hex'); } checkIfHashedTokenMatches(token, hashed) { let hashedValid = this._hashToken(token); return hashedValid === hashed; } _provideTokenPayload(user) { return { id: user.id, scope: 'default', role: user.role }; } _provideTokenOptions() { let config = global.config; return { expiresIn: "10 days", audience: config.jwtOptions.audience, issuer: config.jwtOptions.issuer, algorithm: config.jwtOptions.algorithm }; } } module.exports = AuthHandler;
The
auth.js
creates theissueNewToken
method to create a new token for a logged in user.
In this section, you create the controller methods for every API. The controller methods will read the user input from the API request, then creates the response for the API request.
-
The
controller
directoryNavigate to the
app
directory$ cd ..
Create the
controller
directory$ mkdir controller
-
Navigate to the
controller
directory$ cd controller
-
Create a new file named
auth.js
$ nano auth.js
Add the following content to it:
const BaseController = require(APP_CONTROLLER_PATH + 'base'); const AuthHandler = require(APP_HANDLER_PATH + 'auth'); class AuthController extends BaseController { constructor() { super(); this._authHandler = new AuthHandler(); this._passport = require('passport'); } // Request token by credentials create(req, res, next) { let responseManager = this._responseManager; let that = this; this.authenticate(req, res, next, (user) => { that._authHandler.issueNewToken(req, user, responseManager.getDefaultResponseHandler(res)); }); } authenticate(req, res, next, callback) { let responseManager = this._responseManager; this._passport.authenticate('credentials-auth', function (err, user) { if (err) { responseManager.respondWithError(res, err.status || 401, err.message || ""); } else { callback(user); } })(req, res, next); } authorize(req, res, next){ let responseManager = this._responseManager; let that = this; this._passport.authenticate('jwt-rs-auth', { onVerified: function (token, user) { if(user.role!=="WRITE"){ res.status(403).json("Does not have permission to create a new blog"); }else{ res.status(200).json("Able to create a new blog"); } }, onFailure: function (error) { responseManager.respondWithError(res, error.status || 401, error.message); } })(req, res, next); } authen(req, res, next) { let responseManager = this._responseManager; this._passport.authenticate('jwt-rs-auth', { onVerified: function () { res.status(200).json("Token is valid") }, onFailure: function (error) { responseManager.respondWithError(res, error.status || 401, error.message); } })(req, res, next); } } module.exports = AuthController;
The
auth.js
file defines controller methods for both authentication and authorization APIs. -
Create a new file named
base.js
$ nano base.js
Add the following content to the
base.js
file then save it:const ResponseManager = require(APP_MANAGER_PATH + 'response'); const BaseAutoBindedClass = require(APP_BASE_PACKAGE_PATH + 'base-autobind'); class BaseController extends BaseAutoBindedClass { constructor() { super(); if (new.target === BaseController) { throw new TypeError("Cannot construct BaseController instances directly"); } this._responseManager = ResponseManager; } getAll(req, res) { } get(req, res) { } create(req, res) { } authenticate(req, res, callback) { } } module.exports = BaseController;
The
base.js
file defines the base methods that other controller classes will use. -
The
post.js
file$ nano post.js
Add the following content to the
post.js
file then save it:const axios = require("axios"); const BaseController = require(APP_CONTROLLER_PATH + 'base'); const PostHandler = require(APP_HANDLER_PATH + 'post'); class PostController extends BaseController { constructor() { super(); this._postHandler = new PostHandler(); this._passport = require('passport'); } getAll(req, res, next) { axios.post("http://localhost:3001/verify", { }, { headers: { Authorization: req.headers.authorization } }) .catch(function(error){ req.status('401').json('Token is invalid'); }) .then((response) => { console.log(response); this._postHandler.getAllPosts(req, this._responseManager.getDefaultResponseHandler(res)); }); } create(reqCreate, resCreate, next) { axios.post("http://localhost:3001/verify", { }, { headers: { Authorization: reqCreate.headers.authorization } }) .catch(function(error){ resCreate.status('401').json('Token is invalid'); }) .then((response) => { console.log(response); axios.post("http://localhost:3004/authorize", { }, { headers: { Authorization: reqCreate.headers.authorization } }) .catch(function(error){ resCreate.status('403').json('User does not have permission to create blog post'); }).then((response)=>{ console.log(response); this._postHandler.createNewPost(reqCreate, this._responseManager.getDefaultResponseHandler(resCreate)); }) }); } } module.exports = PostController;
The
post.js
file creates controller methods for creating and retrieving API content. -
The
user.js
file$ nano user.js
Add the following content to the
user.js
file then save it:const BaseController = require(APP_CONTROLLER_PATH + 'base'); const UserHandler = require(APP_HANDLER_PATH + 'user'); const util = require("util"); class UserController extends BaseController { constructor() { super(); this._authHandler = new UserHandler(); this._passport = require('passport'); } create(req, res) { let responseManager = this._responseManager; this.authenticate(req, res, () => { this._authHandler.createNewUser(req, responseManager.getDefaultResponseHandler(res)); }); } authenticate(req, res, callback) { let responseManager = this._responseManager; this._passport.authenticate('secret-key-auth', { onVerified: callback, onFailure: function (error) { responseManager.respondWithError(res, error.status || 401, error.message); } })(req, res); } } module.exports = UserController;
The
user.js
file defines controller methods for the user service.
For the clients to access the API, you need to create the routing definition for every API.
-
The
route
directory$ cd .. $ mkdir route
Navigate to the
route
directory$ cd route
The
route
directory contains routing paths for all the APIs. -
The
v1
directoryAs a best practice, you should version your API. The API routes are located in the
v1
directory.Create a new directory named
v1
inside theroute
directory$ mkdir v1
-
The
index.js
fileCreate a new file named
index.js
inside theroute
directory$ nano index.js
Add the following content to it:
const express = require('express'), router = express.Router(); // API V1 router.use('/v1', require(APP_ROUTE_PATH + 'v1')); module.exports = router;
The
index.js
file is the entry point of theroute
which tells the application to use theroutes
inside thev1
directory. -
The
authentication.js
fileNavigate to the
v1
directory$ cd v1
Create the
authentication.js
file$ nano authentication.js
Add the following content to it:
const router = require('express').Router(); const AuthController = require(APP_CONTROLLER_PATH + 'auth'); let authController = new AuthController(); router.post('/', authController.create); router.post('/verify', authController.authen); module.exports = router;
The
authentication.js
file defines routes for authentication APIs. -
The
authorization.js
fileCreate the
authorization.js
file:$ nano authorization.js
Add the following content to it:
const router = require('express').Router(); const AuthController = require(APP_CONTROLLER_PATH + 'auth'); let authController = new AuthController(); router.post('/authorize', authController.authorize); module.exports = router;
The
authorization.js
file defines the route for the authorization API. -
The
post.js
fileCreate the
post.js
file$ nano post.js
Add the following content to it
const router = require('express').Router(); const PostController = require(APP_CONTROLLER_PATH + 'post'); let postController = new PostController(); router.get('/',postController.getAll); router.post('/', postController.create); module.exports = router;
The
post.js
file defines the routes for the blog APIs. -
The
user.js
fileCreate the
user.js
file$ nano user.js
Add the following content to it
const router = require('express').Router(); const UserController = require(APP_CONTROLLER_PATH + 'user'); let userController = new UserController(); router.post('/', userController.create); module.exports = router;
The
user.js
file defines the routes for the user APIs.
You use pm2
to manage the backend services of the demo application. For pm2
to do that, you need to create the entry point for each service.
-
The
userSvc.js
fileNavigate to the root directory of the project
$ cd $PROJECT_DIRECTORY
Create the
userSvc.js
file$ nano userSvc.js
-
Add the following content to the
userSvc.js
filerequire('./tracing.js'); global.APP_ROOT_PATH = __dirname + '/app/'; // Set other app paths require('./config/global-paths'); // Set config variables global.config = require('./config'); const express = require('express'); const app = express(); // Include dependencies const bodyParser = require('body-parser'); const mongoose = require('mongoose'); mongoose.set("strictQuery", true); const ValidationManager = require(APP_MANAGER_PATH + 'validation'); const authManager = require(APP_MANAGER_PATH + 'auth'); const validationManager = new ValidationManager(); // Connect to DB mongoose.Promise = global.Promise; mongoose.connect(config.db.MONGO_CONNECT_URL); // Use json formatter middleware app.use(bodyParser.json()); app.use(authManager.providePassport().initialize()); // Set Up validation middleware app.use(validationManager.provideDefaultValidator()); // Setup routes const ROUTE_V1_PATH = APP_ROOT_PATH + 'route/' + "v1/"; app.use('/', require(ROUTE_V1_PATH + 'user')); app.listen(process.env.PORT, function(){ console.log("App is running on", process.env.PORT); });
-
Create a new file named
authenSvc.js
at the root directory of the project$ nano authenSvc.js
-
Add the following content to the file and save it
require('./tracing.js'); global.APP_ROOT_PATH = __dirname + '/app/'; // Set other app paths require('./config/global-paths'); // Set config variables global.config = require('./config'); const express = require('express'); const app = express(); // Include dependencies const bodyParser = require('body-parser'); const mongoose = require('mongoose'); mongoose.set("strictQuery", true); const ValidationManager = require(APP_MANAGER_PATH + 'validation'); const authManager = require(APP_MANAGER_PATH + 'auth'); const validationManager = new ValidationManager(); // Connect to DB mongoose.Promise = global.Promise; mongoose.connect(config.db.MONGO_CONNECT_URL); // Use json formatter middleware app.use(bodyParser.json()); app.use(authManager.providePassport().initialize()); // Set Up validation middleware app.use(validationManager.provideDefaultValidator()); // Setup routes const ROUTE_V1_PATH = APP_ROOT_PATH + 'route/' + "v1/"; app.use('/', require(ROUTE_V1_PATH + 'authentication')); app.listen(process.env.PORT, function(){ console.log("App is running on", process.env.PORT); });
The above code defines the entrypoint for the authentication service so that it can make a connection to the MongoDB database or sending traces to the Jaeger service.
-
Create a new file named
authorSvc.js
at the root of the project$ nano authorSvc.js
-
Add the following content to the file and save it:
require('./tracing.js'); global.APP_ROOT_PATH = __dirname + '/app/'; // Set other app paths require('./config/global-paths'); // Set config variables global.config = require('./config'); const express = require('express'); const app = express(); // Include dependencies const bodyParser = require('body-parser'); const mongoose = require('mongoose'); mongoose.set("strictQuery", true); const ValidationManager = require(APP_MANAGER_PATH + 'validation'); const authManager = require(APP_MANAGER_PATH + 'auth'); const validationManager = new ValidationManager(); // Connect to DB mongoose.Promise = global.Promise; mongoose.connect(config.db.MONGO_CONNECT_URL); // Use json formatter middleware app.use(bodyParser.json()); app.use(authManager.providePassport().initialize()); // Set Up validation middleware app.use(validationManager.provideDefaultValidator()); // Setup routes const ROUTE_V1_PATH = APP_ROOT_PATH + 'route/' + "v1/"; app.use('/', require(ROUTE_V1_PATH + 'authorization')); app.listen(process.env.PORT, function(){ console.log("App is running on", process.env.PORT); });
The above code defines the entrypoint for the authorization service so that it can make a connection to the MongoDB database or sending traces to the Jaeger service.
-
Create a new file named
blogSvc.js
at the root of the project$ nano blogSvc.js
-
Add the following content to the file then save it
require('./tracing.js'); global.APP_ROOT_PATH = __dirname + '/app/'; // Set other app paths require('./config/global-paths'); // Set config variables global.config = require('./config'); const express = require('express'); const app = express(); // Include dependencies const bodyParser = require('body-parser'); const mongoose = require('mongoose'); mongoose.set("strictQuery", true); const ValidationManager = require(APP_MANAGER_PATH + 'validation'); const authManager = require(APP_MANAGER_PATH + 'auth'); const validationManager = new ValidationManager(); // Connect to DB mongoose.Promise = global.Promise; mongoose.connect(config.db.MONGO_CONNECT_URL); // Use json formatter middleware app.use(bodyParser.json()); app.use(authManager.providePassport().initialize()); // Set Up validation middleware app.use(validationManager.provideDefaultValidator()); // Setup routes const ROUTE_V1_PATH = APP_ROOT_PATH + 'route/' + "v1/"; app.use('/', require(ROUTE_V1_PATH + 'post')); app.listen(process.env.PORT, function(){ console.log("App is running on", process.env.PORT); });
The above code defines the entrypoint for the blog management service so that it can make a connection to the MongoDB database or sending traces to the Jaeger service.
In this section, you test how the API tracing works with the help of Jaeger.
-
Start the application services
Run the following command to start all the backend services
$ pm2 start
-
Create a new user with
WRITE
permissionRun the following command to create a new user with
WRITE
permission$ curl --location 'localhost:3002' \ --header 'Authorization: example' \ --header 'Content-Type: application/json' \ --data-raw ' { "firstName": "User", "lastName": "Write", "email": "userwithwrite@gmail.com", "password":"userwithwrite", "role":"WRITE" }'
You should see the following response as below:
{"success":true,"message":"","data":{"firstName":"User","lastName":"Write","isActive":true,"role":"WRITE","email":"userwithwrite@gmail.com","_id":"651f32c7e9c1fef066874ea4","dateCreated":"2023-10-05T22:03:51.324Z"},"links":[]}
-
Authenticate the user to get the token
Run the following content to authenticate the user.
$ curl --location 'localhost:3001' \ --header 'Content-Type: application/json' \ --data-raw ' { "email": "userwithwrite@gmail.com", "password":"userwithwrite" }'
You should see the similar output in the console as the one below:
{"success":true,"message":"","data":{"token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY1MWYzMmM3ZTljMWZlZjA2Njg3NGVhNCIsInNjb3BlIjoiZGVmYXVsdCIsInJvbGUiOiJXUklURSIsImlhdCI6MTY5NjU0MzU2NywiZXhwIjoxNjk3NDA3NTY3LCJhdWQiOiJhcGkuYmxvZy5kb25hbGQubmV0IiwiaXNzIjoiYXBpQGRvbmFsZC5uZXQifQ.WkPkbAhXvO5SNJvqDjt8NEHFMC6VCKe4F8CNdQj8GjsuuLe6FUOVz5cHDRkE8tSOdmsv4KakfWuP939qLAlAzz40AMCpULvbXGS70nuzAUIaayrYJg59rRdcubKhmjoXgEQrGIAcFMwqJWDKo981Emb98KTxG_mJX70K9MmHSGo"},"links":[]}
Notice that you have the user
token
value in the response. -
Create a new blog post using the
token
above for authenticationRun the following command to create a new blog post. Replace the
eyJh..
token value with your own value.$ curl --location 'localhost:3003' \ --header 'Authorization: JWT eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY1MWM4ZTYyNzZkZDA0M2U5NTM0MzRkMiIsInNjb3BlIjoiZGVmYXVsdCIsInJvbGUiOiJSRUFEIiwiaWF0IjoxNjk2NDYxOTI3LCJleHAiOjE2OTczMjU5MjcsImF1ZCI6ImFwaS5ibG9nLmRvbmFsZC5uZXQiLCJpc3MiOiJhcGlAZG9uYWxkLm5ldCJ9.Vz0xyTTxtGI_CLvtB0hHbe9GCcWLSBTQefFAHrcu3fawmhYjubEKthhLVdXUJKDJt57dCn0g1fOJubRUfTLzYU8SkUVJOS-qIEauaWeqIkOtd7Mt0NGRvIU8NQ3hSGlNuycFPv9wk5ULTYsYfn287Qz6hmNYc56IlNYStpiwF78' \ --header 'Content-Type: application/json' \ --data '{ "title": "A great post", "content": "A great post man. Please read it carefully. A great post man. Please read it carefully. A great post man. Please read it carefully. ", "authorId": "63eef3ed7aa97bdc8b7f40a1" }'
You should see a similar response as the one below:
{"success":true,"message":"","data":{"title":"A great post","content":"A great post man. Please read it carefully. A great post man. Please read it carefully. A great post man. Please read it carefully.","authorId":"63eef3ed7aa97bdc8b7f40a1","_id":"651f374cd72a8b96eda7abc5","dateCreated":"2023-10-05T22:23:08.982Z","dateModified":"2023-10-05T22:23:08.982Z"},"links":[]}
-
Trace the API request
Open up a browser window and navigate to
http://localhost:16686/
URL. You should see the Jaeger dashboard page.Then search for traces of the
blogSvc
service.Click the first trace. You should see the inner requests of the API request to create a new blog post.
In the inner requests lists, you can see the requests for verifying the user token, authorizing the user permission,and inserting data into the MongoDB database.
Through the article, you learned about how to apply distributed tracing using Jaeger for a Node.js application. To learn more about how to apply distributed tracing with different approaches, visit the How to use OpenTelemetry with Streamlit applications or How to install and use Istio on Kubernetes Engine articles.