From 2c1cbf05512fb559b4b7152bc13b29b379703096 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Wed, 3 May 2023 16:00:32 +0300 Subject: [PATCH 01/13] test-servers --- package-lock.json | 110 +++++++++++- packages/test-servers/package.json | 8 +- packages/test-servers/src/bi-grpc-service.ts | 33 ++++ packages/test-servers/src/bi.proto | 22 +++ packages/test-servers/src/constants.ts | 5 + packages/test-servers/src/email-service.ts | 21 +++ packages/test-servers/src/gateway.ts | 33 ++++ packages/test-servers/src/gig-service.ts | 61 +++++++ packages/test-servers/src/helloworld.proto | 24 --- packages/test-servers/src/index.ts | 169 +++++-------------- packages/test-servers/src/orders-service.ts | 66 ++++++++ packages/test-servers/src/postgres.ts | 50 ++++++ packages/test-servers/src/user-service.ts | 48 ++++++ 13 files changed, 489 insertions(+), 161 deletions(-) create mode 100644 packages/test-servers/src/bi-grpc-service.ts create mode 100644 packages/test-servers/src/bi.proto create mode 100644 packages/test-servers/src/constants.ts create mode 100644 packages/test-servers/src/email-service.ts create mode 100644 packages/test-servers/src/gateway.ts create mode 100644 packages/test-servers/src/gig-service.ts delete mode 100644 packages/test-servers/src/helloworld.proto create mode 100644 packages/test-servers/src/orders-service.ts create mode 100644 packages/test-servers/src/postgres.ts create mode 100644 packages/test-servers/src/user-service.ts diff --git a/package-lock.json b/package-lock.json index b68d919..17aabc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4272,6 +4272,14 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" }, + "node_modules/@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ioredis4": { "name": "@types/ioredis", "version": "4.28.10", @@ -7118,8 +7126,7 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "node_modules/execa": { "version": "5.1.1", @@ -8253,6 +8260,19 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -8267,6 +8287,40 @@ "node": ">= 6" } }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -13632,6 +13686,11 @@ "node": ">=6" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -15869,6 +15928,7 @@ "@traceloop/instrument-opentelemetry": "^0.5.0", "express": "^4.18.2", "google-protobuf": "^3.0.0", + "http-proxy-middleware": "^2.0.6", "pg": "^8.9.0", "typeorm": "^0.3.12", "uuid": "^9.0.0" @@ -18973,6 +19033,7 @@ "@types/uuid": "^9.0.0", "express": "^4.18.2", "google-protobuf": "^3.0.0", + "http-proxy-middleware": "*", "pg": "^8.9.0", "rollup": "^3.20.0", "rollup-plugin-swc3": "^0.8.0", @@ -19233,6 +19294,14 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==" }, + "@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "requires": { + "@types/node": "*" + } + }, "@types/ioredis4": { "version": "npm:@types/ioredis@4.28.10", "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.10.tgz", @@ -21355,8 +21424,7 @@ "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, "execa": { "version": "5.1.1", @@ -22234,6 +22302,16 @@ } } }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -22245,6 +22323,25 @@ "debug": "4" } }, + "http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "dependencies": { + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==" + } + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -26383,6 +26480,11 @@ "resolve": "^1.22.1" } }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", diff --git a/packages/test-servers/package.json b/packages/test-servers/package.json index 10abf9e..367c7ba 100644 --- a/packages/test-servers/package.json +++ b/packages/test-servers/package.json @@ -8,11 +8,8 @@ }, "scripts": { "prebuild": "rm -rf dist", - "build": "rollup -c && cp ./src/helloworld.proto ./dist/helloworld.proto", - "start:orders": "ORDERS_SERVICE=TRUE SERVICE_NAME=orders-service PORT=3000 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:emails": "EMAILS_SERVICE=TRUE SERVICE_NAME=emails-service PORT=3001 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:grpc": "GRPC_SERVICE=TRUE SERVICE_NAME=grpc-service PORT=50051 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start": "concurrently \"npm:start:orders\" \"npm:start:emails\" \"npm:start:grpc\"" + "build": "rollup -c && cp ./src/bi.proto ./dist/bi.proto", + "start": "OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js" }, "devDependencies": { "@types/express": "^4.17.17", @@ -25,6 +22,7 @@ "@traceloop/instrument-opentelemetry": "^0.5.0", "express": "^4.18.2", "google-protobuf": "^3.0.0", + "http-proxy-middleware": "^2.0.6", "pg": "^8.9.0", "typeorm": "^0.3.12", "uuid": "^9.0.0" diff --git a/packages/test-servers/src/bi-grpc-service.ts b/packages/test-servers/src/bi-grpc-service.ts new file mode 100644 index 0000000..f84f0fb --- /dev/null +++ b/packages/test-servers/src/bi-grpc-service.ts @@ -0,0 +1,33 @@ +import grpc from '@grpc/grpc-js'; +import protoLoader from '@grpc/proto-loader'; +import { GRPC_SERVICE_PORT } from './constants'; + +const PROTO_PATH = __dirname + '/bi.proto'; +const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}); +const bi_proto = grpc.loadPackageDefinition(packageDefinition).bi; + +export const biGrpcService = new grpc.Server(); + +function reportBi(call: any, callback: any) { + callback(null, { message: 'Received BI event from ' + call.request.name }); +} +biGrpcService.addService((bi_proto as any).Bi.service, { + reportBi: reportBi, +}); + +const client = new (bi_proto as any).Bi( + `localhost:${GRPC_SERVICE_PORT}`, + grpc.credentials.createInsecure(), +); + +// should be called from other services (makes an rpc call to the bi grpc service) +export const sendBiEvent = (name: string, id: string) => { + client.reportBi({ name: name, id: id }, function (_: any, response: any) { + }); +}; diff --git a/packages/test-servers/src/bi.proto b/packages/test-servers/src/bi.proto new file mode 100644 index 0000000..b13d86c --- /dev/null +++ b/packages/test-servers/src/bi.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_outer_classname = "ReportBiProto"; +option objc_class_prefix = "HLW"; + +package bi; + +service Bi { + rpc ReportBi (ReportBiEvent) returns (ReportBiReply) {} +} + +// The request message containing the BI data. +message ReportBiEvent { + string name = 1; + string id = 2; +} + +// The response message. +message ReportBiReply { + string message = 1; +} diff --git a/packages/test-servers/src/constants.ts b/packages/test-servers/src/constants.ts new file mode 100644 index 0000000..7abe613 --- /dev/null +++ b/packages/test-servers/src/constants.ts @@ -0,0 +1,5 @@ +export const USERS_SERVICE_PORT = 3001, + GIGS_SERVICE_PORT = 3002, + ORDERS_SERVICE_PORT = 3003, + EMAILS_SERVICE_PORT = 3004, + GRPC_SERVICE_PORT = 50051; diff --git a/packages/test-servers/src/email-service.ts b/packages/test-servers/src/email-service.ts new file mode 100644 index 0000000..b41caf9 --- /dev/null +++ b/packages/test-servers/src/email-service.ts @@ -0,0 +1,21 @@ +import express from 'express'; +import axios from 'axios'; +import { EMAILS_SERVICE_PORT } from './constants'; + +export const emailsService = express(); + +emailsService.post('/emails/send', (req, res) => { + res.send('Email sent!'); +}); + +// should be called from other services (makes an http call to the emails service) +export const sendEmail = async (body: any) => { + try { + return await axios.post( + `http://localhost:${EMAILS_SERVICE_PORT}/emails/send`, + body, + ); + } catch (err) { + console.error('Error sending email', err); + } +}; diff --git a/packages/test-servers/src/gateway.ts b/packages/test-servers/src/gateway.ts new file mode 100644 index 0000000..d4e5496 --- /dev/null +++ b/packages/test-servers/src/gateway.ts @@ -0,0 +1,33 @@ +import express from 'express'; +import { createProxyMiddleware } from 'http-proxy-middleware'; +import { + USERS_SERVICE_PORT, + GIGS_SERVICE_PORT, + ORDERS_SERVICE_PORT, +} from './constants'; + +export const gatewayService = express(); + +gatewayService.use( + '/users', + createProxyMiddleware({ + target: `http://localhost:${USERS_SERVICE_PORT}`, + changeOrigin: true, + }), +); + +gatewayService.use( + '/gigs', + createProxyMiddleware({ + target: `http://localhost:${GIGS_SERVICE_PORT}`, + changeOrigin: true, + }), +); + +gatewayService.use( + '/orders', + createProxyMiddleware({ + target: `http://localhost:${ORDERS_SERVICE_PORT}`, + changeOrigin: true, + }), +); diff --git a/packages/test-servers/src/gig-service.ts b/packages/test-servers/src/gig-service.ts new file mode 100644 index 0000000..8139fe3 --- /dev/null +++ b/packages/test-servers/src/gig-service.ts @@ -0,0 +1,61 @@ +import express from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import { initializeDbIfNeeded, postgresDb } from './postgres'; +import { sendEmail } from './email-service'; + +export const gigsService = express(); +gigsService.use(express.json()); + +// required body params: +// - user_name +// - title +gigsService.post('/gigs/create', async (req, res) => { + await initializeDbIfNeeded(); + + const gigId = uuidv4(); + console.log( + `Creating gig ${gigId} with title ${req.body.title} for user ${req.body.user_name}`, + ); + + try { + // check user exists + const userRes = await postgresDb.query( + `SELECT * FROM users WHERE name = '${req.body.user_name}'`, + ); + if (!userRes?.length) { + throw new Error(`User ${req.body.user_name} not found`); + } + const userId = userRes[0].id; + + // check gig doesn't already exist with the same title + const existingGigs = await postgresDb.query( + `SELECT * FROM gigs WHERE user_id = '${userId}' AND title = '${req.body.title}'`, + ); + if (existingGigs?.length) { + throw new Error( + `A gig with the title "${req.body.title}" already exists for user ${req.body.user_name}`, + ); + } + + // create gig + await postgresDb.query( + `INSERT INTO gigs (id, title, user_id) VALUES ('${gigId}', '${req.body.title}', '${userId}')`, + ); + + // send email + sendEmail({ + message: 'Gig created!', + title: req.body.title, + user: req.body.user_name, + id: gigId, + }); + + res.status(201); + res.send({ gigId }); + } catch (err) { + console.error('Error creating gig', err); + + res.status(500); + res.send({ error: 'Error creating gig', message: err.message }); + } +}); diff --git a/packages/test-servers/src/helloworld.proto b/packages/test-servers/src/helloworld.proto deleted file mode 100644 index 688974b..0000000 --- a/packages/test-servers/src/helloworld.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax = "proto3"; - -option java_multiple_files = true; -option java_package = "io.grpc.examples.helloworld"; -option java_outer_classname = "HelloWorldProto"; -option objc_class_prefix = "HLW"; - -package helloworld; - -// The greeting service definition. -service Greeter { - // Sends a greeting - rpc SayHello (HelloRequest) returns (HelloReply) {} -} - -// The request message containing the user's name. -message HelloRequest { - string name = 1; -} - -// The response message containing the greetings -message HelloReply { - string message = 1; -} diff --git a/packages/test-servers/src/index.ts b/packages/test-servers/src/index.ts index f3928e8..a72137c 100644 --- a/packages/test-servers/src/index.ts +++ b/packages/test-servers/src/index.ts @@ -1,140 +1,53 @@ -import axios from 'axios'; -import express from 'express'; -import { DataSource } from 'typeorm'; -import { v4 as uuidv4 } from 'uuid'; import grpc from '@grpc/grpc-js'; -import protoLoader from '@grpc/proto-loader'; - -// --- Protos --- -const PROTO_PATH = __dirname + '/helloworld.proto'; - -const packageDefinition = protoLoader.loadSync(PROTO_PATH, { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, +import { usersService } from './user-service'; +import { gigsService } from './gig-service'; +import { ordersService } from './orders-service'; +import { emailsService } from './email-service'; +import { biGrpcService } from './bi-grpc-service'; +import { gatewayService } from './gateway'; +import { + USERS_SERVICE_PORT, + GIGS_SERVICE_PORT, + ORDERS_SERVICE_PORT, + EMAILS_SERVICE_PORT, + GRPC_SERVICE_PORT, +} from './constants'; + +// --- Initialize Services --- +const PORT = process.env.PORT || 3000; +gatewayService.listen(PORT, () => { + console.log(`Gateway service listening at http://localhost:${PORT}`); }); -const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; -// --- Postgres --- -const ordersDataSource = new DataSource({ - type: 'postgres', - host: process.env.POSTGRES_HOST || 'localhost', - port: process.env.POSTGRES_PORT ? parseInt(process.env.POSTGRES_PORT) : 5432, - username: process.env.POSTGRES_USERNAME || 'postgres', - password: process.env.POSTGRES_PASSWORD || 'postgres', - database: process.env.POSTGRES_DATABASE || 'postgres', +usersService.listen(USERS_SERVICE_PORT, () => { + console.log( + `Users service listening at http://localhost:${USERS_SERVICE_PORT}`, + ); }); -const postgresSchema = process.env.POSTGRES_SCHEMA || 'public'; -let ordersDataSourceInitialized = false; - -const initializeOrdersDatasource = async () => - ordersDataSource - .initialize() - .then(async () => { - console.log('Orders data Source has been initialized!'); - await ordersDataSource.query( - `CREATE TABLE IF NOT EXISTS ${postgresSchema}.orders (id varchar(50), price_in_cents int)`, - ); - ordersDataSourceInitialized = true; - console.log('Orders table has been created!'); - }) - .catch((err) => { - console.error('Error during orders data source initialization', err); - }); - -if (process.env.ORDERS_SERVICE) { - initializeOrdersDatasource(); -} - -// --- Orders Service --- -const ordersService = express(); - -ordersService.post('/orders/create', async (req, res) => { - if (!ordersDataSourceInitialized) { - await initializeOrdersDatasource(); - } - - const orderId = uuidv4(); - console.log('Creating order...'); - - try { - ordersDataSource.query( - `INSERT INTO orders (id, price_in_cents) VALUES ('${orderId}', 1000)`, - ); - } catch (err) { - console.error('Error creating order', err); - console.log( - 'Omitting order creation, please make sure the database is running', - ); - } - - // make http call - console.log('Order created! Sending email...'); - const EMAILS_SERVICE_URL = - process.env.EMAILS_SERVICE_URL || 'http://localhost:3001'; - await axios.post(`${EMAILS_SERVICE_URL}/emails/send`, { - email: 'test', - nestedObject: { test: 'test' }, - }); - - // make grpc call - console.log('Making gRPC call'); - const GRPC_SERVICE_URL = process.env.GRPC_SERVICE_URL || 'localhost:50051'; - const client = new (hello_proto as any).Greeter( - GRPC_SERVICE_URL, - grpc.credentials.createInsecure(), +gigsService.listen(GIGS_SERVICE_PORT, () => { + console.log( + `Gigs service listening at http://localhost:${GIGS_SERVICE_PORT}`, ); - - client.sayHello({ name: 'name' }, function (_: any, response: any) { - console.log('Greeting:', response.message); - }); - - res.send('Order created!'); }); -// --- Emails Service --- -const emailsService = express(); - -emailsService.post('/emails/send', (req, res) => { - console.log('Email sent!'); - res.send('Email sent!'); +ordersService.listen(ORDERS_SERVICE_PORT, () => { + console.log( + `Orders service listening at http://localhost:${ORDERS_SERVICE_PORT}`, + ); }); -// --- gRPC Service --- -const grpcServer = new grpc.Server(); - -function sayHello(call: any, callback: any) { - callback(null, { message: 'Hello ' + call.request.name }); -} -grpcServer.addService((hello_proto as any).Greeter.service, { - sayHello: sayHello, +emailsService.listen(EMAILS_SERVICE_PORT, () => { + console.log( + `Emails service listening at http://localhost:${EMAILS_SERVICE_PORT}`, + ); }); -// --- Initialize Service --- -const PORT = process.env.PORT || 3000; - -if (process.env.ORDERS_SERVICE) { - ordersService.listen(PORT, () => { - console.log(`Orders service listening at http://localhost:${PORT}`); - }); -} - -if (process.env.EMAILS_SERVICE) { - emailsService.listen(PORT, () => { - console.log(`Emails service listening at http://localhost:${PORT}`); - }); -} - -if (process.env.GRPC_SERVICE) { - grpcServer.bindAsync( - `0.0.0.0:${PORT}`, - grpc.ServerCredentials.createInsecure(), - () => { - console.log(`gRPC service listening on port ${PORT}`); - grpcServer.start(); - }, - ); -} +biGrpcService.bindAsync( + `0.0.0.0:${GRPC_SERVICE_PORT}`, + grpc.ServerCredentials.createInsecure(), + () => { + console.log(`gRPC service listening on port ${GRPC_SERVICE_PORT}`); + biGrpcService.start(); + }, +); diff --git a/packages/test-servers/src/orders-service.ts b/packages/test-servers/src/orders-service.ts new file mode 100644 index 0000000..90a6677 --- /dev/null +++ b/packages/test-servers/src/orders-service.ts @@ -0,0 +1,66 @@ +import express from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import { initializeDbIfNeeded, postgresDb } from './postgres'; +import { sendEmail } from './email-service'; +import { sendBiEvent } from './bi-grpc-service'; + +export const ordersService = express(); +ordersService.use(express.json()); + +// required body params: +// - gig_title +// - buyer_name +ordersService.post('/orders/create', async (req, res) => { + await initializeDbIfNeeded(); + + const orderId = uuidv4(); + console.log( + `Creating order ${orderId} with gig title ${req.body.gig_title} for buyer ${req.body.buyer_name}`, + ); + + try { + // check buyer exists + const userRes = await postgresDb.query( + `SELECT * FROM users WHERE name = '${req.body.buyer_name}'`, + ); + if (!userRes?.length) { + throw new Error(`User ${req.body.buyer_name} not found`); + } + const buyerId = userRes[0].id; + + // check gig exists + const existingGigs = await postgresDb.query( + `SELECT * FROM gigs WHERE title = '${req.body.gig_title}'`, + ); + if (!existingGigs?.length) { + throw new Error( + `A gig with the title "${req.body.gig_title}" was not found.`, + ); + } + const gigId = existingGigs[0].id; + const sellerId = existingGigs[0].user_id; + + // create order + postgresDb.query( + `INSERT INTO orders (id, gig_id, seller_id, buyer_id) VALUES ('${orderId}', '${gigId}', '${sellerId}', '${buyerId}}')`, + ); + + sendEmail({ + message: 'Order created!', + gigTitle: req.body.gig_title, + buyerId: buyerId, + sellerId: sellerId, + id: orderId, + }); + + sendBiEvent('order_created', orderId); + + res.status(201); + res.send({ orderId }); + } catch (err) { + console.error('Error creating order', err); + + res.status(500); + res.send({ error: 'Error creating order', message: err.message }); + } +}); diff --git a/packages/test-servers/src/postgres.ts b/packages/test-servers/src/postgres.ts new file mode 100644 index 0000000..c3648ad --- /dev/null +++ b/packages/test-servers/src/postgres.ts @@ -0,0 +1,50 @@ +import { DataSource } from 'typeorm'; + +// --- Postgres --- +export const postgresDb = new DataSource({ + type: 'postgres', + host: process.env.POSTGRES_HOST || 'localhost', + port: process.env.POSTGRES_PORT ? parseInt(process.env.POSTGRES_PORT) : 5432, + username: process.env.POSTGRES_USERNAME || 'postgres', + password: process.env.POSTGRES_PASSWORD || 'postgres', + database: process.env.POSTGRES_DATABASE || 'postgres', +}); + +const postgresSchema = process.env.POSTGRES_SCHEMA || 'public'; +let dbInitialized = false; + +const initializeDb = async () => + postgresDb + .initialize() + .then(async () => { + console.log('Database initialized!'); + try { + await postgresDb.query( + `CREATE TABLE IF NOT EXISTS ${postgresSchema}.users (id varchar(50), name varchar(50))`, + ); + console.log('Users table has been created!'); + + await postgresDb.query( + `CREATE TABLE IF NOT EXISTS ${postgresSchema}.gigs (id varchar(50), user_id varchar(50), title varchar(50))`, + ); + console.log('Gigs table has been created!'); + + await postgresDb.query( + `CREATE TABLE IF NOT EXISTS ${postgresSchema}.orders (id varchar(50), gig_id varchar(50), seller_id varchar(50), buyer_id varchar(50))`, + ); + console.log('Orders table has been created!'); + + dbInitialized = true; + } catch (err) { + console.error('Error creating postgres table', err); + } + }) + .catch((err) => { + console.error('Error during orders data source initialization', err); + }); + +export const initializeDbIfNeeded = async () => { + if (!dbInitialized) { + await initializeDb(); + } +}; diff --git a/packages/test-servers/src/user-service.ts b/packages/test-servers/src/user-service.ts new file mode 100644 index 0000000..44dcbc3 --- /dev/null +++ b/packages/test-servers/src/user-service.ts @@ -0,0 +1,48 @@ +import express from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import { initializeDbIfNeeded, postgresDb } from './postgres'; +import { sendEmail } from './email-service'; +import { sendBiEvent } from './bi-grpc-service'; + +export const usersService = express(); +usersService.use(express.json()); + +// required body params: +// - name +usersService.post('/users/create', async (req, res) => { + await initializeDbIfNeeded(); + + const userId = uuidv4(); + console.log(`Creating user ${userId} with name ${req.body.name}`); + + try { + // check if user exists + const userRes = await postgresDb.query( + `SELECT * FROM users WHERE name = '${req.body.name}'`, + ); + if (userRes?.length) { + throw new Error(`User ${req.body.name} already exists.`); + } + + // create user + await postgresDb.query( + `INSERT INTO users (id, name) VALUES ('${userId}', '${req.body.name}')`, + ); + + sendEmail({ + message: 'User created!', + name: req.body.name, + id: userId, + }); + + sendBiEvent('user_created', userId); + + res.status(201); + res.send({ userId }); + } catch (err) { + console.error('Error creating user', err); + + res.status(500); + res.send({ error: 'Error creating user', message: err.message }); + } +}); From 39a1ac5888a83737c774da39f7ffe63225f8d541 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Wed, 3 May 2023 16:35:06 +0300 Subject: [PATCH 02/13] prettier --- packages/test-servers/src/bi-grpc-service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/test-servers/src/bi-grpc-service.ts b/packages/test-servers/src/bi-grpc-service.ts index f84f0fb..ab70306 100644 --- a/packages/test-servers/src/bi-grpc-service.ts +++ b/packages/test-servers/src/bi-grpc-service.ts @@ -28,6 +28,5 @@ const client = new (bi_proto as any).Bi( // should be called from other services (makes an rpc call to the bi grpc service) export const sendBiEvent = (name: string, id: string) => { - client.reportBi({ name: name, id: id }, function (_: any, response: any) { - }); + client.reportBi({ name: name, id: id }, function (_: any, response: any) {}); }; From ee81fc2922cb074796a9383ea1fcb276b4f8a009 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Wed, 3 May 2023 16:43:40 +0300 Subject: [PATCH 03/13] lint --- packages/test-servers/src/bi-grpc-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-servers/src/bi-grpc-service.ts b/packages/test-servers/src/bi-grpc-service.ts index ab70306..a23d6dc 100644 --- a/packages/test-servers/src/bi-grpc-service.ts +++ b/packages/test-servers/src/bi-grpc-service.ts @@ -28,5 +28,5 @@ const client = new (bi_proto as any).Bi( // should be called from other services (makes an rpc call to the bi grpc service) export const sendBiEvent = (name: string, id: string) => { - client.reportBi({ name: name, id: id }, function (_: any, response: any) {}); + client.reportBi({ name: name, id: id }, function (_: any, response: any) {}); // eslint-disable-line }; From 31c713e080d9da402e4ce4a4bfb9e764e8769e50 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Wed, 3 May 2023 17:10:04 +0300 Subject: [PATCH 04/13] fix --- packages/test-servers/package.json | 8 +++- packages/test-servers/src/gateway.ts | 44 ++++++++--------- packages/test-servers/src/index.ts | 70 +++++++++++++++------------- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/packages/test-servers/package.json b/packages/test-servers/package.json index 367c7ba..70a7125 100644 --- a/packages/test-servers/package.json +++ b/packages/test-servers/package.json @@ -9,7 +9,13 @@ "scripts": { "prebuild": "rm -rf dist", "build": "rollup -c && cp ./src/bi.proto ./dist/bi.proto", - "start": "OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js" + "start:emails": "EMAILS_SERVICE=TRUE SERVICE_NAME=test-servers-emails OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:grpc": "GRPC_SERVICE=TRUE SERVICE_NAME=test-servers-grpc OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:users": "USERS_SERVICE=TRUE SERVICE_NAME=test-servers-users OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:gigs": "GIGS_SERVICE=TRUE SERVICE_NAME=test-servers-gigs OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:orders": "ORDERS_SERVICE=TRUE SERVICE_NAME=test-servers-orders OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:gateway": "GATEWAY_SERVICE=TRUE SERVICE_NAME=test-servers-gateway PORT=3000 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start": "concurrently \"npm:start:users\" \"npm:start:gigs\" \"npm:start:orders\" \"npm:start:gateway\" \"npm:start:emails\" \"npm:start:grpc\"" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/packages/test-servers/src/gateway.ts b/packages/test-servers/src/gateway.ts index d4e5496..5c9bd5a 100644 --- a/packages/test-servers/src/gateway.ts +++ b/packages/test-servers/src/gateway.ts @@ -8,26 +8,28 @@ import { export const gatewayService = express(); -gatewayService.use( - '/users', - createProxyMiddleware({ - target: `http://localhost:${USERS_SERVICE_PORT}`, - changeOrigin: true, - }), -); +if (process.env.GATEWAY_SERVICE) { + gatewayService.use( + '/users', + createProxyMiddleware({ + target: `http://localhost:${USERS_SERVICE_PORT}`, + changeOrigin: true, + }), + ); -gatewayService.use( - '/gigs', - createProxyMiddleware({ - target: `http://localhost:${GIGS_SERVICE_PORT}`, - changeOrigin: true, - }), -); + gatewayService.use( + '/gigs', + createProxyMiddleware({ + target: `http://localhost:${GIGS_SERVICE_PORT}`, + changeOrigin: true, + }), + ); -gatewayService.use( - '/orders', - createProxyMiddleware({ - target: `http://localhost:${ORDERS_SERVICE_PORT}`, - changeOrigin: true, - }), -); + gatewayService.use( + '/orders', + createProxyMiddleware({ + target: `http://localhost:${ORDERS_SERVICE_PORT}`, + changeOrigin: true, + }), + ); +} diff --git a/packages/test-servers/src/index.ts b/packages/test-servers/src/index.ts index a72137c..2c80f00 100644 --- a/packages/test-servers/src/index.ts +++ b/packages/test-servers/src/index.ts @@ -14,40 +14,46 @@ import { } from './constants'; // --- Initialize Services --- -const PORT = process.env.PORT || 3000; -gatewayService.listen(PORT, () => { - console.log(`Gateway service listening at http://localhost:${PORT}`); -}); +const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; +process.env.GATEWAY_SERVICE && + gatewayService.listen(PORT, () => { + console.log(`Gateway service listening at http://localhost:${PORT}`); + }); -usersService.listen(USERS_SERVICE_PORT, () => { - console.log( - `Users service listening at http://localhost:${USERS_SERVICE_PORT}`, - ); -}); +process.env.USERS_SERVICE && + usersService.listen(USERS_SERVICE_PORT, () => { + console.log( + `Users service listening at http://localhost:${USERS_SERVICE_PORT}`, + ); + }); -gigsService.listen(GIGS_SERVICE_PORT, () => { - console.log( - `Gigs service listening at http://localhost:${GIGS_SERVICE_PORT}`, - ); -}); +process.env.GIGS_SERVICE && + gigsService.listen(GIGS_SERVICE_PORT, () => { + console.log( + `Gigs service listening at http://localhost:${GIGS_SERVICE_PORT}`, + ); + }); -ordersService.listen(ORDERS_SERVICE_PORT, () => { - console.log( - `Orders service listening at http://localhost:${ORDERS_SERVICE_PORT}`, - ); -}); +process.env.ORDERS_SERVICE && + ordersService.listen(ORDERS_SERVICE_PORT, () => { + console.log( + `Orders service listening at http://localhost:${ORDERS_SERVICE_PORT}`, + ); + }); -emailsService.listen(EMAILS_SERVICE_PORT, () => { - console.log( - `Emails service listening at http://localhost:${EMAILS_SERVICE_PORT}`, - ); -}); +process.env.EMAILS_SERVICE && + emailsService.listen(EMAILS_SERVICE_PORT, () => { + console.log( + `Emails service listening at http://localhost:${EMAILS_SERVICE_PORT}`, + ); + }); -biGrpcService.bindAsync( - `0.0.0.0:${GRPC_SERVICE_PORT}`, - grpc.ServerCredentials.createInsecure(), - () => { - console.log(`gRPC service listening on port ${GRPC_SERVICE_PORT}`); - biGrpcService.start(); - }, -); +process.env.GRPC_SERVICE && + biGrpcService.bindAsync( + `0.0.0.0:${GRPC_SERVICE_PORT}`, + grpc.ServerCredentials.createInsecure(), + () => { + console.log(`gRPC service listening on port ${GRPC_SERVICE_PORT}`); + biGrpcService.start(); + }, + ); From ed007b92c594254ffc34106f444df553b2401b13 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Wed, 3 May 2023 17:48:52 +0300 Subject: [PATCH 05/13] staging env --- packages/test-servers/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/test-servers/package.json b/packages/test-servers/package.json index 70a7125..c6e0df6 100644 --- a/packages/test-servers/package.json +++ b/packages/test-servers/package.json @@ -9,12 +9,12 @@ "scripts": { "prebuild": "rm -rf dist", "build": "rollup -c && cp ./src/bi.proto ./dist/bi.proto", - "start:emails": "EMAILS_SERVICE=TRUE SERVICE_NAME=test-servers-emails OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:grpc": "GRPC_SERVICE=TRUE SERVICE_NAME=test-servers-grpc OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:users": "USERS_SERVICE=TRUE SERVICE_NAME=test-servers-users OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:gigs": "GIGS_SERVICE=TRUE SERVICE_NAME=test-servers-gigs OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:orders": "ORDERS_SERVICE=TRUE SERVICE_NAME=test-servers-orders OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start:gateway": "GATEWAY_SERVICE=TRUE SERVICE_NAME=test-servers-gateway PORT=3000 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:emails": "EMAILS_SERVICE=TRUE SERVICE_NAME=test-servers-emails OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:grpc": "GRPC_SERVICE=TRUE SERVICE_NAME=test-servers-grpc OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:users": "USERS_SERVICE=TRUE SERVICE_NAME=test-servers-users OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:gigs": "GIGS_SERVICE=TRUE SERVICE_NAME=test-servers-gigs OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:orders": "ORDERS_SERVICE=TRUE SERVICE_NAME=test-servers-orders OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", + "start:gateway": "GATEWAY_SERVICE=TRUE SERVICE_NAME=test-servers-gateway PORT=3000 OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", "start": "concurrently \"npm:start:users\" \"npm:start:gigs\" \"npm:start:orders\" \"npm:start:gateway\" \"npm:start:emails\" \"npm:start:grpc\"" }, "devDependencies": { From 08087de0a5061b6f40be10bdb6c8f8e7e8f204d6 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Wed, 3 May 2023 18:00:44 +0300 Subject: [PATCH 06/13] readme --- packages/test-servers/README.md | 17 +++++++++++++++++ packages/test-servers/public/architecture.png | Bin 0 -> 66582 bytes 2 files changed, 17 insertions(+) create mode 100644 packages/test-servers/README.md create mode 100644 packages/test-servers/public/architecture.png diff --git a/packages/test-servers/README.md b/packages/test-servers/README.md new file mode 100644 index 0000000..e6e759c --- /dev/null +++ b/packages/test-servers/README.md @@ -0,0 +1,17 @@ +# test-servers + +This is a test environment which sends telemetry data (by default to Traceloop servers). +A single container (listening on multiple ports) mimics different microservices that communicate via http and gRPC. + +Services include: + +- gateway +- users service +- gigs service +- orders service +- emails service +- bi grpc service + +## Architecture + +![architecture](./public/architecture.png) diff --git a/packages/test-servers/public/architecture.png b/packages/test-servers/public/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..c06bd27ed15d19cfdfff81619d3100e01cc9a488 GIT binary patch literal 66582 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 Date: Thu, 4 May 2023 20:30:16 +0300 Subject: [PATCH 07/13] env var --- packages/test-servers/src/postgres.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/test-servers/src/postgres.ts b/packages/test-servers/src/postgres.ts index c3648ad..b96fc57 100644 --- a/packages/test-servers/src/postgres.ts +++ b/packages/test-servers/src/postgres.ts @@ -3,14 +3,14 @@ import { DataSource } from 'typeorm'; // --- Postgres --- export const postgresDb = new DataSource({ type: 'postgres', - host: process.env.POSTGRES_HOST || 'localhost', - port: process.env.POSTGRES_PORT ? parseInt(process.env.POSTGRES_PORT) : 5432, - username: process.env.POSTGRES_USERNAME || 'postgres', - password: process.env.POSTGRES_PASSWORD || 'postgres', - database: process.env.POSTGRES_DATABASE || 'postgres', + host: process.env.TEST_SERVERS_POSTGRES_HOST || 'localhost', + port: process.env.TEST_SERVERS_POSTGRES_PORT ? parseInt(process.env.TEST_SERVERS_POSTGRES_PORT) : 5432, + username: process.env.TEST_SERVERS_POSTGRES_USERNAME || 'postgres', + password: process.env.TEST_SERVERS_POSTGRES_PASSWORD || 'postgres', + database: process.env.TEST_SERVERS_POSTGRES_DATABASE || 'postgres', }); -const postgresSchema = process.env.POSTGRES_SCHEMA || 'public'; +const postgresSchema = process.env.TEST_SERVER_POSTGRES_SCHEMA || 'public'; let dbInitialized = false; const initializeDb = async () => From 0f1fea4a83a19bcc2e0ac312ccbb541be1eb69ad Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Fri, 5 May 2023 10:24:35 +0300 Subject: [PATCH 08/13] fix docker file --- packages/test-servers/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/test-servers/Dockerfile b/packages/test-servers/Dockerfile index 6b38d09..b06fce7 100644 --- a/packages/test-servers/Dockerfile +++ b/packages/test-servers/Dockerfile @@ -11,6 +11,7 @@ COPY lerna.json ./ COPY tsconfig.json ./ COPY packages/test-servers/. ./packages/test-servers/ +RUN npm install @traceloop/instrument-opentelemetry RUN npm run build -CMD node ./packages/test-servers/dist/index.js \ No newline at end of file +CMD ["sh", "-c", "cd packages/test-servers && npm run start"] \ No newline at end of file From a933b54634deeb06249a4cdb17b20045105fcb5e Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Fri, 5 May 2023 11:55:28 +0300 Subject: [PATCH 09/13] docker for intel --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 82252f0..cb5d0d8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start:otel-receiver": "lerna run --scope @traceloop/otel-receiver start", "start:test-servers": "lerna run --scope @traceloop/test-servers start", "docker:instrument-opentelemetry": "docker build -f packages/instrument-opentelemetry/Dockerfile . -t instrument-opentelemetry", - "docker:test-servers": "docker build -f packages/test-servers/Dockerfile . -t test-servers", + "docker:test-servers": "docker build --platform linux/amd64 -f packages/test-servers/Dockerfile . -t test-servers", "test": "jest", "test-ci": "concurrently -k --success \"command-1\" --hide 0 \"npm:start:test-servers\" \"npm:test\"", "release": "npm run build && lerna publish --conventional-commits --no-private" From f6c8df13f64f2929281f1ebc8348a6366a76d12b Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Sun, 7 May 2023 13:54:08 +0300 Subject: [PATCH 10/13] ssl ignore --- packages/test-servers/src/postgres.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/test-servers/src/postgres.ts b/packages/test-servers/src/postgres.ts index b96fc57..2c79a26 100644 --- a/packages/test-servers/src/postgres.ts +++ b/packages/test-servers/src/postgres.ts @@ -8,6 +8,9 @@ export const postgresDb = new DataSource({ username: process.env.TEST_SERVERS_POSTGRES_USERNAME || 'postgres', password: process.env.TEST_SERVERS_POSTGRES_PASSWORD || 'postgres', database: process.env.TEST_SERVERS_POSTGRES_DATABASE || 'postgres', + ssl: { + rejectUnauthorized: false + } }); const postgresSchema = process.env.TEST_SERVER_POSTGRES_SCHEMA || 'public'; From b07bf9fc078ae77dc1982dd32e68087b1079f58b Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Sun, 7 May 2023 15:35:14 +0300 Subject: [PATCH 11/13] updates --- packages/test-servers/src/constants.ts | 4 ++ packages/test-servers/src/gig-service.ts | 60 ++++++++++++++-- packages/test-servers/src/index.ts | 12 ++-- packages/test-servers/src/orders-service.ts | 70 +++++++++++++++---- packages/test-servers/src/postgres.ts | 8 ++- .../src/synthetic-traffic/crud-operations.ts | 57 +++++++++++++++ .../src/synthetic-traffic/index.ts | 58 +++++++++++++++ .../src/synthetic-traffic/utils.ts | 5 ++ packages/test-servers/src/user-service.ts | 48 +++++++++++++ 9 files changed, 297 insertions(+), 25 deletions(-) create mode 100644 packages/test-servers/src/synthetic-traffic/crud-operations.ts create mode 100644 packages/test-servers/src/synthetic-traffic/index.ts create mode 100644 packages/test-servers/src/synthetic-traffic/utils.ts diff --git a/packages/test-servers/src/constants.ts b/packages/test-servers/src/constants.ts index 7abe613..b061352 100644 --- a/packages/test-servers/src/constants.ts +++ b/packages/test-servers/src/constants.ts @@ -3,3 +3,7 @@ export const USERS_SERVICE_PORT = 3001, ORDERS_SERVICE_PORT = 3003, EMAILS_SERVICE_PORT = 3004, GRPC_SERVICE_PORT = 50051; + +export const GATEWAY_SERVICE_PORT = process.env.PORT + ? Number(process.env.PORT) + : 3000; diff --git a/packages/test-servers/src/gig-service.ts b/packages/test-servers/src/gig-service.ts index 8139fe3..8899eb6 100644 --- a/packages/test-servers/src/gig-service.ts +++ b/packages/test-servers/src/gig-service.ts @@ -6,24 +6,72 @@ import { sendEmail } from './email-service'; export const gigsService = express(); gigsService.use(express.json()); +gigsService.get('/gigs:gigId', async (req, res) => { + await initializeDbIfNeeded(); + + const gigId = req.params.gigId; + console.log(`Getting gig ${gigId}`); + + try { + const gigRes = await postgresDb.query( + `SELECT * FROM gigs WHERE id = '${gigId}'`, + ); + if (!gigRes?.length) { + throw new Error(`Gig ${gigId} not found`); + } + + res.status(200); + res.send(gigRes[0]); + } catch (err) { + console.error('Error getting gig', err); + res.status(500); + res.send({ error: 'Error getting gig', message: err.message }); + } +}); + +gigsService.delete('/gigs/:gigId', async (req, res) => { + await initializeDbIfNeeded(); + + const gigId = req.params.gigId; + console.log(`Deleting gig ${gigId}`); + + try { + const gigRes = await postgresDb.query( + `SELECT * FROM gigs WHERE id = '${gigId}'`, + ); + if (!gigRes?.length) { + throw new Error(`Gig ${gigId} not found`); + } + + await postgresDb.query(`DELETE FROM gigs WHERE id = '${gigId}'`); + + res.status(200); + res.send({ message: `Gig ${gigId} deleted` }); + } catch (err) { + console.error('Error deleting gig', err); + res.status(500); + res.send({ error: 'Error deleting gig', message: err.message }); + } +}); + // required body params: -// - user_name +// - user_id // - title gigsService.post('/gigs/create', async (req, res) => { await initializeDbIfNeeded(); const gigId = uuidv4(); console.log( - `Creating gig ${gigId} with title ${req.body.title} for user ${req.body.user_name}`, + `Creating gig ${gigId} with title ${req.body.title} for user ${req.body.user_id}`, ); try { // check user exists const userRes = await postgresDb.query( - `SELECT * FROM users WHERE name = '${req.body.user_name}'`, + `SELECT * FROM users WHERE id = '${req.body.user_id}'`, ); if (!userRes?.length) { - throw new Error(`User ${req.body.user_name} not found`); + throw new Error(`User ${req.body.user_id} not found`); } const userId = userRes[0].id; @@ -33,7 +81,7 @@ gigsService.post('/gigs/create', async (req, res) => { ); if (existingGigs?.length) { throw new Error( - `A gig with the title "${req.body.title}" already exists for user ${req.body.user_name}`, + `A gig with the title "${req.body.title}" already exists for user ${req.body.user_id}`, ); } @@ -46,7 +94,7 @@ gigsService.post('/gigs/create', async (req, res) => { sendEmail({ message: 'Gig created!', title: req.body.title, - user: req.body.user_name, + user: req.body.user_id, id: gigId, }); diff --git a/packages/test-servers/src/index.ts b/packages/test-servers/src/index.ts index 2c80f00..f4ebef9 100644 --- a/packages/test-servers/src/index.ts +++ b/packages/test-servers/src/index.ts @@ -5,20 +5,24 @@ import { ordersService } from './orders-service'; import { emailsService } from './email-service'; import { biGrpcService } from './bi-grpc-service'; import { gatewayService } from './gateway'; +import { initializeSyntheticTraffic } from './synthetic-traffic'; import { USERS_SERVICE_PORT, GIGS_SERVICE_PORT, ORDERS_SERVICE_PORT, EMAILS_SERVICE_PORT, GRPC_SERVICE_PORT, + GATEWAY_SERVICE_PORT, } from './constants'; // --- Initialize Services --- -const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; process.env.GATEWAY_SERVICE && - gatewayService.listen(PORT, () => { - console.log(`Gateway service listening at http://localhost:${PORT}`); - }); + gatewayService.listen(GATEWAY_SERVICE_PORT, () => { + console.log( + `Gateway service listening at http://localhost:${GATEWAY_SERVICE_PORT}`, + ); + }) && + initializeSyntheticTraffic(); process.env.USERS_SERVICE && usersService.listen(USERS_SERVICE_PORT, () => { diff --git a/packages/test-servers/src/orders-service.ts b/packages/test-servers/src/orders-service.ts index 90a6677..e4a9679 100644 --- a/packages/test-servers/src/orders-service.ts +++ b/packages/test-servers/src/orders-service.ts @@ -7,35 +7,81 @@ import { sendBiEvent } from './bi-grpc-service'; export const ordersService = express(); ordersService.use(express.json()); +ordersService.get('/orders/:orderId', async (req, res) => { + await initializeDbIfNeeded(); + + const orderId = req.params.orderId; + console.log(`Getting order ${orderId}`); + + try { + const orderRes = await postgresDb.query( + `SELECT * FROM orders WHERE id = '${orderId}'`, + ); + if (!orderRes?.length) { + throw new Error(`Order ${orderId} not found`); + } + + res.status(200); + res.send(orderRes[0]); + } catch (err) { + console.error('Error getting order', err); + res.status(500); + res.send({ error: 'Error getting order', message: err.message }); + } +}); + +ordersService.delete('/orders/:orderId', async (req, res) => { + await initializeDbIfNeeded(); + + const orderId = req.params.orderId; + console.log(`Deleting order ${orderId}`); + + try { + const orderRes = await postgresDb.query( + `SELECT * FROM orders WHERE id = '${orderId}'`, + ); + if (!orderRes?.length) { + throw new Error(`Order ${orderId} not found`); + } + + await postgresDb.query(`DELETE FROM orders WHERE id = '${orderId}'`); + + res.status(200); + res.send({ message: `Order ${orderId} deleted` }); + } catch (err) { + console.error('Error deleting order', err); + res.status(500); + res.send({ error: 'Error deleting order', message: err.message }); + } +}); + // required body params: -// - gig_title -// - buyer_name +// - gig_id +// - buyer_id ordersService.post('/orders/create', async (req, res) => { await initializeDbIfNeeded(); const orderId = uuidv4(); console.log( - `Creating order ${orderId} with gig title ${req.body.gig_title} for buyer ${req.body.buyer_name}`, + `Creating order ${orderId} with gig id ${req.body.gig_id} for buyer ${req.body.buyer_id}`, ); try { // check buyer exists const userRes = await postgresDb.query( - `SELECT * FROM users WHERE name = '${req.body.buyer_name}'`, + `SELECT * FROM users WHERE id = '${req.body.buyer_id}'`, ); if (!userRes?.length) { - throw new Error(`User ${req.body.buyer_name} not found`); + throw new Error(`User ${req.body.buyer_id} not found`); } const buyerId = userRes[0].id; // check gig exists const existingGigs = await postgresDb.query( - `SELECT * FROM gigs WHERE title = '${req.body.gig_title}'`, + `SELECT * FROM gigs WHERE id = '${req.body.gig_id}'`, ); if (!existingGigs?.length) { - throw new Error( - `A gig with the title "${req.body.gig_title}" was not found.`, - ); + throw new Error(`A gig with the id "${req.body.gig_id}" was not found.`); } const gigId = existingGigs[0].id; const sellerId = existingGigs[0].user_id; @@ -47,9 +93,9 @@ ordersService.post('/orders/create', async (req, res) => { sendEmail({ message: 'Order created!', - gigTitle: req.body.gig_title, - buyerId: buyerId, - sellerId: sellerId, + gigId, + buyerId, + sellerId, id: orderId, }); diff --git a/packages/test-servers/src/postgres.ts b/packages/test-servers/src/postgres.ts index 2c79a26..9c31400 100644 --- a/packages/test-servers/src/postgres.ts +++ b/packages/test-servers/src/postgres.ts @@ -4,13 +4,15 @@ import { DataSource } from 'typeorm'; export const postgresDb = new DataSource({ type: 'postgres', host: process.env.TEST_SERVERS_POSTGRES_HOST || 'localhost', - port: process.env.TEST_SERVERS_POSTGRES_PORT ? parseInt(process.env.TEST_SERVERS_POSTGRES_PORT) : 5432, + port: process.env.TEST_SERVERS_POSTGRES_PORT + ? parseInt(process.env.TEST_SERVERS_POSTGRES_PORT) + : 5432, username: process.env.TEST_SERVERS_POSTGRES_USERNAME || 'postgres', password: process.env.TEST_SERVERS_POSTGRES_PASSWORD || 'postgres', database: process.env.TEST_SERVERS_POSTGRES_DATABASE || 'postgres', ssl: { - rejectUnauthorized: false - } + rejectUnauthorized: false, + }, }); const postgresSchema = process.env.TEST_SERVER_POSTGRES_SCHEMA || 'public'; diff --git a/packages/test-servers/src/synthetic-traffic/crud-operations.ts b/packages/test-servers/src/synthetic-traffic/crud-operations.ts new file mode 100644 index 0000000..7a55731 --- /dev/null +++ b/packages/test-servers/src/synthetic-traffic/crud-operations.ts @@ -0,0 +1,57 @@ +import axios from 'axios'; +import { GATEWAY_SERVICE_PORT } from '../constants'; +import { randNumber } from './utils'; +import { postgresDb, initializeDbIfNeeded } from '../postgres'; + +export const createUser = async () => { + const userName = `synthetic_user-${randNumber(1, 1000)}`; + const user = await axios.post<{ userId: string }>( + `http://localhost:${GATEWAY_SERVICE_PORT}/users/create`, + { + name: userName, + }, + ); + + return user.data; +}; + +export const createGig = async (sellerId: string) => { + const gigTitle = `synthetic_gig-${randNumber(1, 1000)}`; + const gig = await axios.post<{ gigId: string }>( + `http://localhost:${GATEWAY_SERVICE_PORT}/gigs/create`, + { + title: gigTitle, + user_id: sellerId, + }, + ); + + return gig.data; +}; + +export const createOrder = async (gigId: string, buyerId: string) => { + const order = await axios.post<{ orderId: string }>( + `http://localhost:${GATEWAY_SERVICE_PORT}/orders/create`, + { + gig_id: gigId, + buyer_id: buyerId, + }, + ); + + return order.data; +}; + +export const deleteOrder = async (orderId: string) => { + console.log(`Deleting order ${orderId}`); + await initializeDbIfNeeded(); + await postgresDb.query(`DELETE FROM orders WHERE id = '${orderId}'`); +}; + +export const deleteUser = async (userId: string) => { + await initializeDbIfNeeded(); + await postgresDb.query(`DELETE FROM users WHERE id = '${userId}'`); +}; + +export const deleteGig = async (gigId: string) => { + await initializeDbIfNeeded(); + await postgresDb.query(`DELETE FROM gigs WHERE id = '${gigId}'`); +}; diff --git a/packages/test-servers/src/synthetic-traffic/index.ts b/packages/test-servers/src/synthetic-traffic/index.ts new file mode 100644 index 0000000..d862a39 --- /dev/null +++ b/packages/test-servers/src/synthetic-traffic/index.ts @@ -0,0 +1,58 @@ +import { wait } from './utils'; +import { + createUser, + createGig, + createOrder, + deleteGig, + deleteOrder, + deleteUser, +} from './crud-operations'; + +const INTERVAL_MINUTES = 5; +const OPERATION_INTERVAL_MS = 5000; + +let sellerId, buyerId, gigId, orderId: string; + +export const initializeSyntheticTraffic = async () => { + console.log('Synthetic traffic started'); + syntheticTrafficFlow(); + setInterval(syntheticTrafficFlow, INTERVAL_MINUTES * 60 * 1000); +}; + +const syntheticTrafficFlow = async () => { + try { + const seller = await createUser(); + sellerId = seller.userId; + + await wait(OPERATION_INTERVAL_MS); + + const buyer = await createUser(); + buyerId = buyer.userId; + + await wait(OPERATION_INTERVAL_MS); + + const gig = await createGig(sellerId); + gigId = gig.gigId; + + await wait(OPERATION_INTERVAL_MS); + + const order = await createOrder(gigId, buyerId); + orderId = order.orderId; + + await wait(OPERATION_INTERVAL_MS); + + await cleanup(); + } catch (err) { + console.error('Synthetic traffic error', err); + + await cleanup(); + } +}; + +const cleanup = async () => + await Promise.allSettled([ + deleteOrder(orderId), + deleteGig(gigId), + deleteUser(buyerId), + deleteUser(sellerId), + ]); diff --git a/packages/test-servers/src/synthetic-traffic/utils.ts b/packages/test-servers/src/synthetic-traffic/utils.ts new file mode 100644 index 0000000..46efc03 --- /dev/null +++ b/packages/test-servers/src/synthetic-traffic/utils.ts @@ -0,0 +1,5 @@ +export const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const randNumber = (min: number, max: number) => { + return Math.floor(Math.random() * (max - min + 1)) + min; +}; diff --git a/packages/test-servers/src/user-service.ts b/packages/test-servers/src/user-service.ts index 44dcbc3..f1f7ed5 100644 --- a/packages/test-servers/src/user-service.ts +++ b/packages/test-servers/src/user-service.ts @@ -7,6 +7,54 @@ import { sendBiEvent } from './bi-grpc-service'; export const usersService = express(); usersService.use(express.json()); +usersService.get('/users/:userId', async (req, res) => { + await initializeDbIfNeeded(); + + const userId = req.params.userId; + console.log(`Getting user ${userId}`); + + try { + const userRes = await postgresDb.query( + `SELECT * FROM users WHERE id = '${userId}'`, + ); + if (!userRes?.length) { + throw new Error(`User ${userId} not found`); + } + + res.status(200); + res.send(userRes[0]); + } catch (err) { + console.error('Error getting user', err); + res.status(500); + res.send({ error: 'Error getting user', message: err.message }); + } +}); + +usersService.delete('/users/:userId', async (req, res) => { + await initializeDbIfNeeded(); + + const userId = req.params.userId; + console.log(`Deleting user ${userId}`); + + try { + const userRes = await postgresDb.query( + `SELECT * FROM users WHERE id = '${userId}'`, + ); + if (!userRes?.length) { + throw new Error(`User ${userId} not found`); + } + + await postgresDb.query(`DELETE FROM users WHERE id = '${userId}'`); + + res.status(200); + res.send({ message: `User ${userId} deleted` }); + } catch (err) { + console.error('Error deleting user', err); + res.status(500); + res.send({ error: 'Error deleting user', message: err.message }); + } +}); + // required body params: // - name usersService.post('/users/create', async (req, res) => { From 5aeee3da21fa46b8728811c4855bfc4c7289d969 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Sun, 7 May 2023 16:06:35 +0300 Subject: [PATCH 12/13] better --- packages/test-servers/src/postgres.ts | 5 +---- packages/test-servers/src/synthetic-traffic/index.ts | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/test-servers/src/postgres.ts b/packages/test-servers/src/postgres.ts index 9c31400..3e3a49d 100644 --- a/packages/test-servers/src/postgres.ts +++ b/packages/test-servers/src/postgres.ts @@ -27,17 +27,14 @@ const initializeDb = async () => await postgresDb.query( `CREATE TABLE IF NOT EXISTS ${postgresSchema}.users (id varchar(50), name varchar(50))`, ); - console.log('Users table has been created!'); await postgresDb.query( `CREATE TABLE IF NOT EXISTS ${postgresSchema}.gigs (id varchar(50), user_id varchar(50), title varchar(50))`, ); - console.log('Gigs table has been created!'); await postgresDb.query( `CREATE TABLE IF NOT EXISTS ${postgresSchema}.orders (id varchar(50), gig_id varchar(50), seller_id varchar(50), buyer_id varchar(50))`, ); - console.log('Orders table has been created!'); dbInitialized = true; } catch (err) { @@ -45,7 +42,7 @@ const initializeDb = async () => } }) .catch((err) => { - console.error('Error during orders data source initialization', err); + console.error('Error during data source initialization', err); }); export const initializeDbIfNeeded = async () => { diff --git a/packages/test-servers/src/synthetic-traffic/index.ts b/packages/test-servers/src/synthetic-traffic/index.ts index d862a39..a34e95f 100644 --- a/packages/test-servers/src/synthetic-traffic/index.ts +++ b/packages/test-servers/src/synthetic-traffic/index.ts @@ -14,6 +14,8 @@ const OPERATION_INTERVAL_MS = 5000; let sellerId, buyerId, gigId, orderId: string; export const initializeSyntheticTraffic = async () => { + await wait(OPERATION_INTERVAL_MS); + console.log('Synthetic traffic started'); syntheticTrafficFlow(); setInterval(syntheticTrafficFlow, INTERVAL_MINUTES * 60 * 1000); From c86ed0f739cf5d78eea7551ad5584115a7244a89 Mon Sep 17 00:00:00 2001 From: Tomer <4tomer.friedman@gmail.com> Date: Sun, 7 May 2023 18:25:26 +0300 Subject: [PATCH 13/13] dont instrument synthetic --- packages/instrument-opentelemetry/src/tracing.ts | 3 +++ packages/test-servers/package.json | 3 ++- packages/test-servers/src/index.ts | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/instrument-opentelemetry/src/tracing.ts b/packages/instrument-opentelemetry/src/tracing.ts index 77a9ac4..8cf8a0b 100644 --- a/packages/instrument-opentelemetry/src/tracing.ts +++ b/packages/instrument-opentelemetry/src/tracing.ts @@ -14,6 +14,9 @@ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { ? new ProtoExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, timeoutMillis: 100, + // headers: { + // authorization: 'testtttt', + // }, }) : new GRPCExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, diff --git a/packages/test-servers/package.json b/packages/test-servers/package.json index c6e0df6..5c49521 100644 --- a/packages/test-servers/package.json +++ b/packages/test-servers/package.json @@ -15,7 +15,8 @@ "start:gigs": "GIGS_SERVICE=TRUE SERVICE_NAME=test-servers-gigs OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", "start:orders": "ORDERS_SERVICE=TRUE SERVICE_NAME=test-servers-orders OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", "start:gateway": "GATEWAY_SERVICE=TRUE SERVICE_NAME=test-servers-gateway PORT=3000 OTEL_EXPORTER_OTLP_ENDPOINT=https://api-staging.traceloop.dev/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", - "start": "concurrently \"npm:start:users\" \"npm:start:gigs\" \"npm:start:orders\" \"npm:start:gateway\" \"npm:start:emails\" \"npm:start:grpc\"" + "start:uninstrumented": "NOT_INSTRUMENTED=TRUE node dist/index.js", + "start": "concurrently \"npm:start:users\" \"npm:start:gigs\" \"npm:start:orders\" \"npm:start:gateway\" \"npm:start:emails\" \"npm:start:grpc\" \"npm:start:uninstrumented\"" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/packages/test-servers/src/index.ts b/packages/test-servers/src/index.ts index f4ebef9..241a64e 100644 --- a/packages/test-servers/src/index.ts +++ b/packages/test-servers/src/index.ts @@ -21,8 +21,7 @@ process.env.GATEWAY_SERVICE && console.log( `Gateway service listening at http://localhost:${GATEWAY_SERVICE_PORT}`, ); - }) && - initializeSyntheticTraffic(); + }); process.env.USERS_SERVICE && usersService.listen(USERS_SERVICE_PORT, () => { @@ -61,3 +60,5 @@ process.env.GRPC_SERVICE && biGrpcService.start(); }, ); + +process.env.NOT_INSTRUMENTED && initializeSyntheticTraffic();