[WIP] Port RobotShop to CF

This commit is contained in:
Michele Mancioppi
2019-08-24 06:10:08 +02:00
parent 6555f8de34
commit 48032a561b
48 changed files with 1553 additions and 103 deletions

119
CF/README.md Normal file
View File

@@ -0,0 +1,119 @@
# RobotShop on PCF [WIP]
## Requirements
The following tiles up and running in the PCF foundation:
- [Pivotal Application Service](https://network.pivotal.io/products/elastic-runtime)
- [RabbitMQ for PCF](https://network.pivotal.io/products/p-rabbitmq/)
- [Redis for PCF](https://network.pivotal.io/products/p-redis/)
- [MySQL for PCF](https://network.pivotal.io/products/pivotal-mysql/)
- [MongoDB Enterprise Service for PCF](https://network.pivotal.io/products/mongodb-enterprise-service/)
- [Instana Microservices Application Monitoring for PCF](https://network.pivotal.io/products/instana-microservices-application-monitoring/)
## Create an organization and a space
```sh
cf create-org stan
cf create-space -o stan robotshop
```
## Ensure routes are available
The following routes must be available for use:
- `web.<domain>`
- `ratings.<domain>`
- `cart.<domain>`
- `catalogue.<domain>`
- `shipping.<domain>`
- `user.<domain>`
- `payment.<domain>`
- `dispatch.<domain>`
## Set up the services
```sh
cf target -o stan -s robotshop
cf cs p.mysql db-small mysql
cf cs mongodb-odb replica_set_small mongodb
cf cs p.redis cache-small redis
cf cs p.rabbitmq single-node-3.7 rabbitmq
```
## First app push
RobotShop relies on specific binding names between services and apps, so we first push the apps without creating instances (all instance counts in the manifest are `0`).
From the root of the repo:
```sh
cf push -f CF/manifest.yml
```
## Bind services
```sh
cf bind-service mongo-init mongodb --binding-name catalogue_database
cf bind-service ratings mysql --binding-name ratings_database
cf bind-service catalogue mongodb --binding-name catalog_database
cf bind-service cart redis --binding-name cart_cache
cf bind-service shipping mysql --binding-name shipping_database
cf bind-service user redis --binding-name users_cache
cf bind-service user mongodb --binding-name users_database
cf bind-service payment rabbitmq --binding-name dispatch_queue
cf bind-service dispatch rabbitmq --binding-name dispatch_queue
```
## Init MongoDB
```sh
cf run-task mongo-init 'node init-db.js' --name "Init MongoDB"
```
## Init MySQL
This one is not automated yet, as the `mysql-init` task app chokes on the `init.sql`.
Something that "works" is to `bosh ssh` on the MySQL and run the database init via commandline (`mysql` is on the path) using the credentials one finds by doing `cf env shipping`.
First import `init.sql` and then the following:
```sql
CREATE DATABASE ratings
DEFAULT CHARACTER SET 'utf8';
USE ratings;
CREATE TABLE ratings (
sku varchar(80) NOT NULL,
avg_rating DECIMAL(3, 2) NOT NULL,
rating_count INT NOT NULL,
PRIMARY KEY (sku)
) ENGINE=InnoDB;
```
The above is the content of the [ratings sql init script](../mysql/20-ratings.sql) minus the unnecessary user privs.
## Configure EUM
Create a website in Instana.
Edit the `web/static/eum.html` file accordingly, specifically replacing the values of the `reportingUrl` and `key` ienums.
## Spin up the containers
```sh
cf cart -i 1 web
cf cart -i 1 ratings
cf scale -i 1 cart
cf scale -i 1 catalogue
cf scale -i 1 shipping
cf scale -i 1 user
cf scale -i 1 payment
cf scale -i 1 dispatch
```

113
CF/manifest.yml Normal file
View File

@@ -0,0 +1,113 @@
---
applications:
# cf cs p.mysql db-small mysql
# cf cs mongodb-odb replica_set_small mongodb
# cf cs p.redis cache-small redis
# cf cs p.rabbitmq single-node-3.7 rabbitmq
# cf bind-service mongo-init mongodb --binding-name catalogue_database
# cf run-task mongo-init 'node init-db.js' --name "Init MongoDB"
### DOES NOT WORK, IMPORTED MYSEL BY HAND LOGGING INTO MYSQL INSTANCE CUZ YOLO
### cf bind-service mysql-init mysql --binding-name shipping_database
### cf run-task mysql-init 'node init-db.js' --name "Init MySQL"
# cf bind-service ratings mysql --binding-name ratings_database
# cf scale -i 1 ratings
# cf bind-service catalogue mongodb --binding-name catalog_database
# cf scale -i 1 catalogue
# cf bind-service cart redis --binding-name cart_cache
# cf scale -i 1 cart
# cf bind-service shipping mysql --binding-name shipping_database
# cf scale -i 1 shipping
# cf bind-service user redis --binding-name users_cache
# cf bind-service user mongodb --binding-name users_database
# cf scale -i 1 user
# cf bind-service payment rabbitmq --binding-name dispatch_queue
# cf scale -i 1 payment
# cf bind-service dispatch rabbitmq --binding-name dispatch_queue
# cf scale -i 1 dispatch
###
### "Task" applications to setup services, see https://docs.cloudfoundry.org/devguide/using-tasks.html
###
- name: mongo-init
path: ../CF/mongo-init
memory: 128M
no-route: true # It's a CF Task, does not need a route
health-check-type: process
instances: 0
# - name: mysql-init
# path: ../CF/mysql-init
# memory: 512M
# no-route: true # It's a CF Task, does not need a route
# health-check-type: process
# instances: 0
###
### Productive apps
###
- name: cart
path: ../cart/
memory: 128M
env:
CATALOGUE_HTTPS: true
instances: 0
- name: catalogue
path: ../catalogue/
instances: 0
- name: dispatch
path: ../dispatch/src
memory: 128M
health-check-type: process
env:
GOPACKAGENAME: github.com/instana/robot-shop/dispatch
instances: 0
- name: payment
path: ../payment/
memory: 256M
command: python payment.py
env:
AUTOWRAPT_BOOTSTRAP: instana
instances: 0
- name: ratings
path: ../ratings/app
memory: 128MB
env:
CATALOGUE_USE_HTTPS: true
instances: 0
- name: shipping
path: ../shipping/target/shipping-1.0-jar-with-dependencies.jar
memory: 1G
instances: 0
- name: user
path: ../user/
memory: 128MB
instances: 1
- name: web
path: ../web/
memory: 128M
buildpacks:
- https://github.com/cloudfoundry/nginx-buildpack.git
env:
INSTANA_EUM_KEY: OT8yjQ_eSPm84a4owlq5Hw
INSTANA_EUM_REPORTING_URL: https://eum-release-fullstack-0-us-west-2.instana.io
instances: 0

109
CF/mongo-init/init-db.js Normal file
View File

@@ -0,0 +1,109 @@
/*jshint esversion: 6 */
const mongoClient = require('mongodb').MongoClient;
var mongoURL = 'mongodb://mongodb:27017/catalogue';
if (process.env.VCAP_SERVICES) {
connectionDetails = null;
for (let [key, value] of Object.entries(JSON.parse(process.env.VCAP_SERVICES))) {
connectionDetails = value.find(function(binding) {
return 'catalogue_database' == binding.binding_name && binding.credentials;
}).credentials;
if (connectionDetails) {
mongoURL = connectionDetails.uri;
break;
}
}
} else if (process.env.MONGO_URL) {
mongoURL = process.env.MONGO_URL;
}
if (!mongoURL) {
throw new Error('MongoDB connection data missing');
}
mongoClient.connect(mongoURL, (error, db) => {
if(error) {
console.log('Cannot connect to MongoDB', error);
process.exit(42);
} else {
console.log('Creating products collection');
products = db.collection('products');
products.insertMany([
{sku: 'HAL-1', name: 'HAL', description: 'Sorry Dave, I cant do that', price: 2001, instock: 2, categories: ['Artificial Intelligence']},
{sku: 'PB-1', name: 'Positronic Brain', description: 'Highly advanced sentient processing unit with the laws of robotics burned in', price: 200, instock: 0, categories: ['Artificial Intelligence']},
{sku: 'ROB-1', name: 'Robbie', description: 'Large mechanical workhorse, crude but effective. Comes in handy when you are lost in space', price: 1200, instock: 12, categories: ['Robot']},
{sku: 'EVE-1', name: 'Eve', description: 'Extraterrestrial Vegetation Evaluator', price: 5000, instock: 10, categories: ['Robot']},
{sku: 'C3P0', name: 'C3P0', description: 'Protocol android', price: 700, instock: 1, categories: ['Robot']},
{sku: 'R2D2', name: 'R2D2', description: 'R2 maintenance robot and secret messenger. Help me Obi Wan', price: 1400, instock: 1, categories: ['Robot']},
{sku: 'K9', name: 'K9', description: 'Time travelling companion at heel', price: 300, instock: 12, categories: ['Robot']},
{sku: 'RD-10', name: 'Kryten', description: 'Red Drawf crew member', price: 700, instock: 5, categories: ['Robot']},
{sku: 'HHGTTG', name: 'Marvin', description: 'Marvin, your paranoid android. Brain the size of a planet', price: 42, instock: 48, categories: ['Robot']},
{sku: 'STAN-1', name: 'Stan', description: 'APM guru', price: 50, instock: 1000, categories: ['Robot', 'Artificial Intelligence']},
{sku: 'STNG', name: 'Mr Data', description: 'Could be R. Daneel Olivaw? Protype positronic brain android', price: 1000, instock: 0, categories: ['Robot']}
]);
//
// Users
//
console.log('Creating users collection');
const users = db.collection('users');
console.log('Starting users import');
users.insertMany([
{name: 'user', password: 'password', email: 'user@me.com'},
{name: 'stan', password: 'bigbrain', email: 'stan@instana.com'}
]);
// unique index on the name
users.createIndex(
{name: 1},
{unique: true}
);
console.log('Users imported');
console.log('Creating catalogue collection');
const catalogue = db.collection('catalogue');
console.log('Starting catalogue import');
catalogue.insertMany([
{sku: 'HAL-1', name: 'HAL', description: 'Sorry Dave, I cant do that', price: 2001, instock: 2, categories: ['Artificial Intelligence']},
{sku: 'PB-1', name: 'Positronic Brain', description: 'Highly advanced sentient processing unit with the laws of robotics burned in', price: 200, instock: 0, categories: ['Artificial Intelligence']},
{sku: 'ROB-1', name: 'Robbie', description: 'Large mechanical workhorse, crude but effective. Comes in handy when you are lost in space', price: 1200, instock: 12, categories: ['Robot']},
{sku: 'EVE-1', name: 'Eve', description: 'Extraterrestrial Vegetation Evaluator', price: 5000, instock: 10, categories: ['Robot']},
{sku: 'C3P0', name: 'C3P0', description: 'Protocol android', price: 700, instock: 1, categories: ['Robot']},
{sku: 'R2D2', name: 'R2D2', description: 'R2 maintenance robot and secret messenger. Help me Obi Wan', price: 1400, instock: 1, categories: ['Robot']},
{sku: 'K9', name: 'K9', description: 'Time travelling companion at heel', price: 300, instock: 12, categories: ['Robot']},
{sku: 'RD-10', name: 'Kryten', description: 'Red Drawf crew member', price: 700, instock: 5, categories: ['Robot']},
{sku: 'HHGTTG', name: 'Marvin', description: 'Marvin, your paranoid android. Brain the size of a planet', price: 42, instock: 48, categories: ['Robot']},
{sku: 'STAN-1', name: 'Stan', description: 'APM guru', price: 50, instock: 1000, categories: ['Robot', 'Artificial Intelligence']},
{sku: 'STNG', name: 'Mr Data', description: 'Could be R. Daneel Olivaw? Protype positronic brain android', price: 1000, instock: 0, categories: ['Robot']}
]);
// full text index for searching
catalogue.createIndex({
name: "text",
description: "text"
});
// unique index for product sku
catalogue.createIndex(
{ sku: 1 },
{ unique: true }
);
console.log('Products imported');
console.log('All done');
}
});

View File

@@ -0,0 +1,14 @@
{
"name": "mongodb-init",
"version": "1.0.0",
"description": "Init RobotShop's MongoDB database",
"main": "init-db.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "SteveW",
"license": "Apache-2.0",
"dependencies": {
"mongodb": "^2.2.33"
}
}

34
CF/mysql-init/init-db.js Normal file
View File

@@ -0,0 +1,34 @@
/*jshint esversion: 6 */
(function () {
'use strict';
if (!process.env.VCAP_SERVICES) {
throw new Error('No services bound (VCAP_SERVICES env var not found)');
}
var connectionDetails;
for (let [key, value] of Object.entries(JSON.parse(process.env.VCAP_SERVICES))) {
connectionDetails = value.find(function(binding) {
return 'shipping_database' == binding.binding_name && binding.credentials;
}).credentials;
if (connectionDetails) {
break;
}
}
if (!connectionDetails) {
throw new Error('Cannot extract MySQL connection details: ' + process.env.VCAP_SERVICES);
}
require('mysql-import').config({
host: connectionDetails.hostname,
user: connectionDetails.username,
password: connectionDetails.password,
database: connectionDetails.name,
onerror: err=>console.log(err.message)
}).import('init.sql')
.then(() => console.log('Database imported'));
}());

147
CF/mysql-init/init.sql Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
{
"name": "mysql-init",
"version": "1.0.0",
"description": "Init RobotShop's MySQL database",
"main": "init-db.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "SteveW",
"license": "Apache-2.0",
"dependencies": {
"mysql-import": "^2.0.2"
}
}

View File

@@ -69,7 +69,7 @@ To deploy the Instana agent to Kubernetes, just use the [helm](https://github.co
If you are having difficulties get helm running with your K8s install it is most likely due to RBAC, most K8s now have RBAC enabled by default. Therefore helm requires a [service account](https://github.com/helm/helm/blob/master/docs/rbac.md) to have permission to do stuff.
## Acessing the Store
## Accessing the Store
If you are running the store locally via *docker-compose up* then, the store front is available on localhost port 8080 [http://localhost:8080](http://localhost:8080/)
If you are running the store on Kubernetes via minikube then, to make the store front accessible edit the *web* service definition and change the type to *NodePort* and add a port entry *nodePort: 30080*.

View File

@@ -16,6 +16,6 @@
"pino": "^5.10.8",
"express-pino-logger": "^4.0.0",
"pino-pretty": "^2.5.0",
"instana-nodejs-sensor": "^1.28.0"
"@instana/collector": "^1.28.0"
}
}

View File

@@ -1,4 +1,6 @@
const instana = require('instana-nodejs-sensor');
/*jshint esversion: 6 */
const instana = require('@instana/collector');
// init tracing
// MUST be done before loading anything else!
instana({
@@ -16,8 +18,63 @@ const expPino = require('express-pino-logger');
var redisConnected = false;
var redisHost = process.env.REDIS_HOST || 'redis'
var catalogueHost = process.env.CATALOGUE_HOST || 'catalogue'
var redisHost;
var redisPassword;
var redisPort;
if (process.env.VCAP_SERVICES) {
connectionDetails = null;
console.log('Env var \'VCAP_SERVICES\' found, scanning for \'cart_cache\' service binding');
for (let [key, value] of Object.entries(JSON.parse(process.env.VCAP_SERVICES))) {
try {
binding = value.find(function(binding) {
return 'cart_cache' == binding.binding_name && binding.credentials;
});
if (!binding) {
continue;
}
connectionDetails = binding.credentials;
if (connectionDetails) {
redisHost = connectionDetails.host;
redisPassword = connectionDetails.password;
redisPort = connectionDetails.port;
console.log('Redis URI for \'cart_cache\' service binding found in \'VCAP_SERVICES\'');
break;
}
} catch (err) {
console.log('Cannot process key \'' + key + '\' of \'VCAP_SERVICES\'', err);
throw err;
}
}
} else if (process.env.REDIS_HOST) {
redisHost = process.env.REDIS_HOST;
redisPort = 6379;
console.log('Redis host found in \'REDIS_HOST\': ' + redisHost);
} else {
redisHost = 'redis';
redisPort = 6379;
console.log('Using default Redis host and port');
}
var catalogueHost = process.env.CATALOGUE_HOST || 'catalogue';
if (process.env.VCAP_APPLICATION) {
vcapApplication = JSON.parse(process.env.VCAP_APPLICATION);
applicationName = vcapApplication.application_name;
applicationUri = vcapApplication.application_uris[0];
catalogueHost = 'catalogue' + applicationUri.substring(applicationUri.indexOf('.'));
}
const catalogueUrl = (process.env.CATALOGUE_HTTPS ? 'https' : 'http') + '://' + catalogueHost;
const logger = pino({
level: 'info',
@@ -183,7 +240,7 @@ app.get('/update/:id/:sku/:qty', (req, res) => {
// check quantity
var qty = parseInt(req.params.qty);
if(isNaN(qty)) {
req.log.warn('quanity not a number');
req.log.warn('quantity not a number');
res.status(400).send('quantity must be a number');
return;
} else if(qty < 0) {
@@ -328,7 +385,7 @@ function calcTax(total) {
function getProduct(sku) {
return new Promise((resolve, reject) => {
request('http://' + catalogueHost + ':8080/product/' + sku, (err, res, body) => {
request(catalogueUrl + '/product/' + sku, (err, res, body) => {
if(err) {
reject(err);
} else if(res.statusCode != 200) {
@@ -355,9 +412,15 @@ function saveCart(id, cart) {
});
}
if (!redisHost) {
throw new Error('Redis connection data missing');
}
// connect to Redis
var redisClient = redis.createClient({
host: redisHost
host: redisHost,
password: redisPassword,
port: redisPort
});
redisClient.on('error', (e) => {
@@ -369,7 +432,7 @@ redisClient.on('ready', (r) => {
});
// fire it up!
const port = process.env.CART_SERVER_PORT || '8080';
const port = process.env.SERVER_PORT || '8080';
app.listen(port, () => {
logger.info('Started on port', port);
});

View File

@@ -15,6 +15,6 @@
"pino": "^5.10.8",
"express-pino-logger": "^4.0.0",
"pino-pretty": "^2.5.0",
"instana-nodejs-sensor": "^1.28.0"
"@instana/collector": "^1.28.0"
}
}

View File

@@ -1,4 +1,6 @@
const instana = require('instana-nodejs-sensor');
/*jshint esversion: 6 */
const instana = require('@instana/collector');
// init tracing
// MUST be done before loading anything else!
instana({
@@ -8,7 +10,6 @@ instana({
});
const mongoClient = require('mongodb').MongoClient;
const mongoObjectID = require('mongodb').ObjectID;
const bodyParser = require('body-parser');
const express = require('express');
const pino = require('pino');
@@ -24,8 +25,7 @@ const expLogger = expPino({
});
// MongoDB
var db;
var collection;
var productsCollection;
var mongoConnected = false;
const app = express();
@@ -52,7 +52,7 @@ app.get('/health', (req, res) => {
// all products
app.get('/products', (req, res) => {
if(mongoConnected) {
collection.find({}).toArray().then((products) => {
productsCollection.find({}).toArray().then((products) => {
res.json(products);
}).catch((e) => {
req.log.error('ERROR', e);
@@ -67,7 +67,7 @@ app.get('/products', (req, res) => {
// product by SKU
app.get('/product/:sku', (req, res) => {
if(mongoConnected) {
collection.findOne({sku: req.params.sku}).then((product) => {
productsCollection.findOne({sku: req.params.sku}).then((product) => {
req.log.info('product', product);
if(product) {
res.json(product);
@@ -87,7 +87,7 @@ app.get('/product/:sku', (req, res) => {
// products in a category
app.get('/products/:cat', (req, res) => {
if(mongoConnected) {
collection.find({ categories: req.params.cat }).sort({ name: 1 }).toArray().then((products) => {
productsCollection.find({ categories: req.params.cat }).sort({ name: 1 }).toArray().then((products) => {
if(products) {
res.json(products);
} else {
@@ -106,7 +106,7 @@ app.get('/products/:cat', (req, res) => {
// all categories
app.get('/categories', (req, res) => {
if(mongoConnected) {
collection.distinct('categories').then((categories) => {
productsCollection.distinct('categories').then((categories) => {
res.json(categories);
}).catch((e) => {
req.log.error('ERROR', e);
@@ -121,7 +121,7 @@ app.get('/categories', (req, res) => {
// search name and description
app.get('/search/:text', (req, res) => {
if(mongoConnected) {
collection.find({ '$text': { '$search': req.params.text }}).toArray().then((hits) => {
productsCollection.find({ '$text': { '$search': req.params.text }}).toArray().then((hits) => {
res.json(hits);
}).catch((e) => {
req.log.error('ERROR', e);
@@ -136,13 +136,56 @@ app.get('/search/:text', (req, res) => {
// set up Mongo
function mongoConnect() {
return new Promise((resolve, reject) => {
var mongoURL = process.env.MONGO_URL || 'mongodb://mongodb:27017/catalogue';
mongoClient.connect(mongoURL, (error, _db) => {
var mongoURL;
if (process.env.VCAP_SERVICES) {
connectionDetails = null;
console.log('Env var \'VCAP_SERVICES\' found, scanning for \'catalogue_database\' service binding');
for (let [key, value] of Object.entries(JSON.parse(process.env.VCAP_SERVICES))) {
try {
binding = value.find(function(binding) {
return 'catalogue_database' == binding.binding_name && binding.credentials;
});
if (!binding) {
continue;
}
connectionDetails = binding.credentials;
if (connectionDetails) {
mongoURL = connectionDetails.uri;
console.log('MongoDB URI for \'catalogue_database\' service binding found in \'VCAP_SERVICES\'');
break;
}
} catch (err) {
console.log('Cannot process key \'' + key + '\' of \'VCAP_SERVICES\'', err);
throw err;
}
}
} else if (process.env.MONGO_URL) {
mongoURL = process.env.MONGO_URL;
console.log('MongoDB URI found in \'MONGO_URL\': ' + mongoURL);
} else {
mongoURL = 'mongodb://mongodb:27017/catalogue';
console.log('Using default MongoDB URI');
}
if (!mongoURL) {
throw new Error('MongoDB connection data missing');
}
mongoClient.connect(mongoURL, (error, db) => {
if(error) {
reject(error);
} else {
db = _db;
collection = db.collection('products');
productsCollection = db.collection('products');
resolve('connected');
}
});
@@ -155,7 +198,7 @@ function mongoLoop() {
mongoConnected = true;
logger.info('MongoDB connected');
}).catch((e) => {
logger.error('ERROR', e);
logger.error('An error occurred', e);
setTimeout(mongoLoop, 2000);
});
}

View File

@@ -8,12 +8,15 @@ import (
"math/rand"
"strconv"
"encoding/json"
"strings"
"github.com/streadway/amqp"
"github.com/instana/go-sensor"
ot "github.com/opentracing/opentracing-go"
ext "github.com/opentracing/opentracing-go/ext"
otlog "github.com/opentracing/opentracing-go/log"
"github.com/cloudfoundry-community/go-cfenv"
)
const (
@@ -160,7 +163,40 @@ func main() {
if !ok {
amqpHost = "rabbitmq"
}
amqpUri = fmt.Sprintf("amqp://guest:guest@%s:5672/", amqpHost)
amqpUser, ok := os.LookupEnv("AMQP_USER")
if !ok {
amqpHost = "guest"
}
amqpPwd, ok := os.LookupEnv("AMQP_PWD")
if !ok {
amqpPwd = "guest"
}
amqpPort, ok := os.LookupEnv("AMQP_PORT")
if !ok {
amqpPort = "5672"
}
amqpUri = fmt.Sprintf("amqp://%s:%s@%s:%s/", amqpUser, amqpPwd , amqpHost, amqpPort)
if cfenv.IsRunningOnCF() {
appEnv, err := cfenv.Current()
if err != nil {
panic(err)
}
services := appEnv.Services
for _, service := range services {
for _, serviceBinding := range service {
bindingName, ok := serviceBinding.CredentialString("binding_name")
if ok {
if strings.Compare(bindingName, "dispatch_queue") == 0 {
amqpUri, _ = serviceBinding.CredentialString("uri")
}
}
}
}
}
// get error threshold from environment
errorPercent = 0

View File

@@ -6,12 +6,14 @@ import uuid
import json
import requests
import traceback
import re
import opentracing as ot
import opentracing.ext.tags as tags
from flask import Flask
from flask import request
from flask import jsonify
from rabbitmq import Publisher
from cfenv import AppEnv
app = Flask(__name__)
@@ -41,9 +43,20 @@ def pay(id):
span.log_kv({'id': id})
span.log_kv({'cart': cart})
user_service_uri = 'http://user:8080'
cart_service_uri = 'http://cart:8080'
if 'VCAP_SERVICES' in os.environ:
env = AppEnv()
application_name = env.app['application_name']
application_uri = env.app['application_uris'][0]
hostname = re.sub(r'^\w+\.', '', application_uri)
user_service_uri = 'https://user.{hostname}'.format(hostname=hostname)
cart_service_uri = 'https://cart.{hostname}'.format(hostname=hostname)
# check user exists
try:
req = requests.get('http://{user}:8080/check/{id}'.format(user=USER, id=id))
req = requests.get('{uri}/check/{id}'.format(uri=user_service_uri, id=id), verify=False)
except requests.exceptions.RequestException as err:
app.logger.error(err)
return str(err), 500
@@ -78,9 +91,10 @@ def pay(id):
# add to order history
if not anonymous_user:
try:
req = requests.post('http://{user}:8080/order/{id}'.format(user=USER, id=id),
req = requests.post('{user_uri}/order/{id}'.format(user_uri=user_service_uri, id=id),
data=json.dumps({'orderid': orderid, 'cart': cart}),
headers={'Content-Type': 'application/json'})
headers={'Content-Type': 'application/json'},
verify=False)
app.logger.info('order history returned {}'.format(req.status_code))
except requests.exceptions.RequestException as err:
app.logger.error(err)
@@ -88,7 +102,7 @@ def pay(id):
# delete cart
try:
req = requests.delete('http://{cart}:8080/cart/{id}'.format(cart=CART, id=id));
req = requests.delete('{cart_uri}/cart/{id}'.format(cart_uri=cart_service_uri, id=id), verify=False);
app.logger.info('cart delete returned {}'.format(req.status_code))
except requests.exceptions.RequestException as err:
app.logger.error(err)
@@ -115,9 +129,10 @@ def queueOrder(order):
tscope.span.log_kv({'orderid': order.get('orderid')})
with ot.tracer.start_active_span('rabbitmq', child_of=tscope.span,
tags={
# 'address': Publisher.HOST,
'messaging_bus.address': publisher._uri,
'exchange': Publisher.EXCHANGE,
'sort': 'publish',
'address': Publisher.HOST,
'key': Publisher.ROUTING_KEY
}
) as scope:

View File

@@ -2,19 +2,30 @@ import json
import pika
import os
from cfenv import AppEnv
class Publisher:
HOST = os.getenv('AMQP_HOST', 'rabbitmq')
VIRTUAL_HOST = '/'
EXCHANGE='robot-shop'
TYPE='direct'
ROUTING_KEY = 'orders'
def __init__(self, logger):
self._logger = logger
self._params = pika.connection.ConnectionParameters(
host=self.HOST,
virtual_host=self.VIRTUAL_HOST,
credentials=pika.credentials.PlainCredentials('guest', 'guest'))
if 'VCAP_SERVICES' in os.environ:
env = AppEnv()
amqp_service = env.get_service(binding_name='dispatch_queue')
self._uri = amqp_service.credentials.get('uri')
else:
self._uri = 'ampq://{user}:{pwd}@{host}:{port}/{vhost}'.format(
host=os.getenv('AMQP_HOST', 'rabbitmq'),
port=os.getenv('AMQP_PORT', '5672'),
vhost='/',
user=os.getenv('AMQP_USER', 'guest'),
pwd=os.getenv('AMQP_PWD', 'guest'))
self._params = pika.URLParameters(self._uri)
self._conn = None
self._channel = None

View File

@@ -1,4 +1,5 @@
Flask
requests
pika
cfenv
instana

View File

@@ -1,7 +1,7 @@
# Use composer to install dependencies
FROM composer AS build
COPY composer.json /app/
COPY app/composer.json /app/
RUN composer install
@@ -13,7 +13,7 @@ FROM php:7.2-apache
RUN docker-php-ext-install pdo_mysql
# relax permissions on status
COPY status.conf /etc/apache2/mods-available/status.conf
COPY app/status.conf /etc/apache2/mods-available/status.conf
# Enable Apache mod_rewrite and status
RUN a2enmod rewrite && a2enmod status
@@ -22,5 +22,4 @@ WORKDIR /var/www/html
# copy dependencies from previous step
COPY --from=build /app/vendor/ /var/www/html/vendor/
COPY html/ /var/www/html
COPY app/html/*.php /var/www/html

View File

@@ -0,0 +1,5 @@
{
"PHP_VERSION": "{PHP_72_LATEST}",
"LIBDIR": "htdocs/vendor",
"COMPOSER_VENDOR_DIR": "htdocs/vendor"
}

View File

@@ -0,0 +1,2 @@
extension=pdo.so
extension=pdo_mysql.so

View File

@@ -0,0 +1,2 @@
#extension=instana-x64-1.3.2.so
instana.socket=10.0.4.8:16816

View File

@@ -2,5 +2,5 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule api/(.*)$ api.php?request=$1 [QSA,NC,L]
RewriteRule (.*)$ api.php?request=$1 [QSA,NC,L]
</IfModule>

220
ratings/app/api.php Normal file
View File

@@ -0,0 +1,220 @@
<?php
require_once 'API.class.php';
use Monolog\Logger;
class RatingsAPI extends API {
public function __construct($request, $origin) {
parent::__construct($request);
// Logging
$this->logger = new Logger('RatingsAPI');
$this->logger->pushHandler($this->logHandler);
}
protected function health() {
$this->logger->info('health OK');
return 'OK';
}
protected function dump() {
$data = array();
$data['method'] = $this->method;
$data['verb'] = $this->verb;
$data = array_merge($data, array('args' => $this->args));
return $data;
}
// GET ratings/fetch/sku
protected function fetch() {
if($this->method == 'GET' && isset($this->verb) && count($this->args) == 0) {
$sku = $this->verb;
if(! $this->_checkSku($sku)) {
throw new Exception("$sku not found", 404);
}
$data = $this->_getRating($sku);
return $data;
} else {
$this->logger->warn('fetch rating - bad request');
throw new Exception('Bad request: fetch', 400);
}
}
// POST ratings/rate/sku/score
protected function rate() {
if($this->method == 'POST' && isset($this->verb) && count($this->args) == 1) {
$sku = $this->verb;
$score = intval($this->args[0]);
$score = min(max(1, $score), 5);
if(! $this->_checkSku($sku)) {
throw new Exception("$sku not found", 404);
}
$rating = $this->_getRating($sku);
if($rating['avg_rating'] == 0) {
// not rated yet
$this->_insertRating($sku, $score);
} else {
// iffy maths
$newAvg = (($rating['avg_rating'] * $rating['rating_count']) + $score) / ($rating['rating_count'] + 1);
$this->_updateRating($sku, $newAvg, $rating['rating_count'] + 1);
}
} else {
$this->logger->warn('set rating - bad request');
throw new Exception('Bad request: ' . implode($this->args), 400);
}
return 'OK';
}
private function _getRating($sku) {
$db = $this->_dbConnect();
if($db) {
$stmt = $db->prepare('select avg_rating, rating_count from ratings where sku = ?');
if($stmt->execute(array($sku))) {
$data = $stmt->fetch();
if($data) {
// for some reason avg_rating is return as a string
$data['avg_rating'] = floatval($data['avg_rating']);
return $data;
} else {
// nicer to return an empty record than throw 404
return array('avg_rating' => 0, 'rating_count' => 0);
}
} else {
$this->logger->error('failed to query data');
throw new Exception('Failed to query data', 500);
}
} else {
$this->logger->error('database connection error');
throw new Exception('Database connection error', 500);
}
}
private function _updateRating($sku, $score, $count) {
$db = $this->_dbConnect();
if($db) {
$stmt = $db->prepare('update ratings set avg_rating = ?, rating_count = ? where sku = ?');
if(! $stmt->execute(array($score, $count, $sku))) {
$this->logger->error('failed to update rating');
throw new Exception('Failed to update data', 500);
}
} else {
$this->logger->error('database connection error');
throw new Exception('Database connection error', 500);
}
}
private function _insertRating($sku, $score) {
$db = $this->_dbConnect();
if($db) {
$stmt = $db->prepare('insert into ratings(sku, avg_rating, rating_count) values(?, ?, ?)');
if(! $stmt->execute(array($sku, $score, 1))) {
$this->logger->error('failed to insert data');
throw new Exception('Failed to insert data', 500);
}
} else {
$this->logger->error('database connection error');
throw new Exception('Database connection error', 500);
}
}
private function _dbConnect() {
$dsn = getenv('PDO_URL') ? getenv('PDO_URL') : 'mysql:host=mysql;dbname=ratings;charset=utf8mb4';
$username = 'ratings';
$password = 'iloveit';
if($vcapServicesJson = getenv('VCAP_SERVICES')) {
$vcapServices = json_decode($vcapServicesJson, true);
foreach ($vcapServices as $key => $value) {
foreach ($value as $serviceBinding){
if ($serviceBinding['binding_name'] == 'ratings_database') {
$credentials = $serviceBinding['credentials'];
$dsn = 'mysql:host=' . $credentials['hostname'] . ';port=' . $credentials['port'] . ';dbname=' . $credentials['name'];
$username = $credentials['username'];
$password = $credentials['password'];
break;
}
}
}
}
$opt = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
);
$db = false;
try {
$db = new PDO($dsn, $username, $password, $opt);
} catch (PDOException $e) {
$msg = $e->getMessage();
$this->logger->error("Database error $msg");
$db = false;
}
return $db;
}
private function _endsWith($subject, $token) {
$tokenLength = strlen($token);
if ($tokenLength == 0) {
return true;
}
return (substr($subject, -$tokenLength) === $token);
}
// check sku exists in product catalogue
private function _checkSku($sku) {
$url = getenv('CATALOGUE_URL') ? getenv('CATALOGUE_URL') : 'http://catalogue:8080/';
if ($vcapApplicationJson = getenv('VCAP_APPLICATION')) {
$vcapApplication = json_decode($vcapApplicationJson, true);
$applicationName = $vcapApplication['application_name'];
$applicationUri = $vcapApplication['application_uris'][0];
$count = 1;
$url = str_replace($applicationName, "catalogue", $applicationUri, $count);
if (!$this->_endsWith($url, '/')) {
$url = (getenv('CATALOGUE_USE_HTTPS') ? 'https' : 'http') . '://' . $url . '/';
}
}
$url = $url . 'product/' . $sku;
$opt = array(
CURLOPT_RETURNTRANSFER => true,
);
$curl = curl_init($url);
curl_setopt_array($curl, $opt);
$data = curl_exec($curl);
if(! $data) {
$this->logger->error('failed to connect to catalogue');
throw new Exception('Failed to connect to catalogue at: ' . $url, 500);
}
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$this->logger->info("catalogue status $status");
curl_close($curl);
return $status == 200;
}
}
if(!array_key_exists('HTTP_ORIGIN', $_SERVER)) {
$_SERVER['HTTP_ORIGIN'] = $_SERVER['SERVER_NAME'];
}
try {
$API = new RatingsAPI($_REQUEST['request'], $_SERVER['HTTP_ORIGIN']);
echo $API->processAPI();
} catch(Exception $e) {
echo json_encode(Array('error' => $e->getMessage()));
}
?>

143
ratings/app/composer.lock generated Normal file
View File

@@ -0,0 +1,143 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8511edda340e04039f72f12feb7f3171",
"packages": [
{
"name": "monolog/monolog",
"version": "1.24.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
"reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/log": "~1.0"
},
"provide": {
"psr/log-implementation": "1.0.0"
},
"require-dev": {
"aws/aws-sdk-php": "^2.4.9 || ^3.0",
"doctrine/couchdb": "~1.0@dev",
"graylog2/gelf-php": "~1.0",
"jakub-onderka/php-parallel-lint": "0.9",
"php-amqplib/php-amqplib": "~2.4",
"php-console/php-console": "^3.1.3",
"phpunit/phpunit": "~4.5",
"phpunit/phpunit-mock-objects": "2.3.0",
"ruflin/elastica": ">=0.90 <3.0",
"sentry/sentry": "^0.13",
"swiftmailer/swiftmailer": "^5.3|^6.0"
},
"suggest": {
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
"ext-mongo": "Allow sending log messages to a MongoDB server",
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
"mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
"php-console/php-console": "Allow sending log messages to Google Chrome",
"rollbar/rollbar": "Allow sending log messages to Rollbar",
"ruflin/elastica": "Allow sending log messages to an Elastic Search server",
"sentry/sentry": "Allow sending log messages to a Sentry server"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Monolog\\": "src/Monolog"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
"homepage": "http://github.com/Seldaek/monolog",
"keywords": [
"log",
"logging",
"psr-3"
],
"time": "2018-11-05T09:00:11+00:00"
},
{
"name": "psr/log",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2018-11-20T15:27:04+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

39
shipping/shipping.iml Normal file
View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/target/classes" />
<output-test url="file://$MODULE_DIR$/target/test-classes" />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: com.sparkjava:spark-core:2.7.2" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.13" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-server:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: javax.servlet:javax.servlet-api:3.1.0" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-http:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-util:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-io:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-webapp:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-xml:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-servlet:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-security:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-server:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-common:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-client:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty:jetty-client:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-servlet:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.eclipse.jetty.websocket:websocket-api:9.4.8.v20171121" level="project" />
<orderEntry type="library" name="Maven: org.slf4j:slf4j-simple:1.7.25" level="project" />
<orderEntry type="library" name="Maven: c3p0:c3p0:0.9.1.2" level="project" />
<orderEntry type="library" name="Maven: mysql:mysql-connector-java:5.1.45" level="project" />
<orderEntry type="library" name="Maven: commons-dbutils:commons-dbutils:1.7" level="project" />
<orderEntry type="library" name="Maven: com.google.code.gson:gson:2.8.2" level="project" />
<orderEntry type="library" name="Maven: org.apache.httpcomponents:httpclient:4.5.5" level="project" />
<orderEntry type="library" name="Maven: org.apache.httpcomponents:httpcore:4.4.9" level="project" />
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
<orderEntry type="library" name="Maven: commons-codec:commons-codec:1.10" level="project" />
</component>
</module>

View File

@@ -1,5 +1,6 @@
package org.steveww.spark;
import com.google.gson.*;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import spark.Spark;
@@ -16,48 +17,75 @@ import org.apache.http.params.HttpParams;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.MapListHandler;
import org.apache.commons.dbutils.DbUtils;
import com.google.gson.Gson;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Types;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
public class Main {
private static String CART_URL = null;
private static String JDBC_URL = null;
private static Logger logger = LoggerFactory.getLogger(Main.class);
private static ComboPooledDataSource cpds = null;
private static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
private static final ComboPooledDataSource CPDS = new ComboPooledDataSource();
public static void main(String[] args) {
// Get ENV configuration values
CART_URL = String.format("http://%s/shipping/", System.getenv("CART_ENDPOINT") != null ? System.getenv("CART_ENDPOINT") : "cart");
JDBC_URL = String.format("jdbc:mysql://%s/cities?useSSL=false&autoReconnect=true", System.getenv("DB_HOST") != null ? System.getenv("DB_HOST") : "mysql");
final String cartUrl;
final String jdbcUrl;
final boolean isCloudFoundry = System.getenv("VCAP_APPLICATION") != null;
if (isCloudFoundry) {
// Cloud Foundry
final JsonParser jsonParser = new JsonParser();
jdbcUrl = getJdbcURLFromVCAPServices(jsonParser);
if (System.getenv("CART_ENDPOINT") != null) {
cartUrl = System.getenv("CART_ENDPOINT");
} else {
final JsonObject vcapApplication = jsonParser.parse(System.getenv("VCAP_APPLICATION")).getAsJsonObject();
final JsonArray appUris = vcapApplication.get("application_uris").getAsJsonArray();
if (appUris == null || appUris.size() == 0) {
throw new IllegalStateException("Cannot retrieve application URIs from 'VCAP_APPLICATION'");
}
final String applicationName = vcapApplication.get("application_name").getAsString();
final String applicationURI = appUris.get(0).getAsString();
cartUrl = "https://" + applicationURI.replace(applicationName, "cart") + "/shipping/";
}
} else {
cartUrl = String.format("http://%s/shipping/", System.getenv("CART_ENDPOINT") != null ? System.getenv("CART_ENDPOINT") : "cart");
jdbcUrl = String.format("jdbc:mysql://%s/cities?useSSL=false&autoReconnect=true", System.getenv("DB_HOST") != null ? System.getenv("DB_HOST") : "mysql");
}
//
// Create database connector
// TODO - might need a retry loop here
//
try {
cpds = new ComboPooledDataSource();
cpds.setDriverClass( "com.mysql.jdbc.Driver" ); //loads the jdbc driver
cpds.setJdbcUrl( JDBC_URL );
cpds.setUser("shipping");
cpds.setPassword("secret");
CPDS.setDriverClass( "com.mysql.jdbc.Driver" ); //loads the jdbc driver
CPDS.setJdbcUrl( jdbcUrl );
if (!isCloudFoundry) {
CPDS.setUser("shipping");
CPDS.setPassword("secret");
}
// some config
cpds.setMinPoolSize(5);
cpds.setAcquireIncrement(5);
cpds.setMaxPoolSize(20);
cpds.setMaxStatements(180);
CPDS.setMinPoolSize(5);
CPDS.setAcquireIncrement(5);
CPDS.setMaxPoolSize(20);
CPDS.setMaxStatements(180);
}
catch(Exception e) {
logger.error("Database Exception", e);
LOGGER.error("Database Exception", e);
}
// Spark
@@ -71,7 +99,7 @@ public class Main {
data = queryToJson("select count(*) as count from cities");
res.header("Content-Type", "application/json");
} catch(Exception e) {
logger.error("count", e);
LOGGER.error("count", e);
res.status(500);
data = "ERROR";
}
@@ -86,7 +114,7 @@ public class Main {
data = queryToJson(query);
res.header("Content-Type", "application/json");
} catch(Exception e) {
logger.error("codes", e);
LOGGER.error("codes", e);
res.status(500);
data = "ERROR";
}
@@ -99,11 +127,11 @@ public class Main {
String data;
try {
String query = "select uuid, name from cities where country_code = ?";
logger.info("Query " + query);
LOGGER.info("Query " + query);
data = queryToJson(query, req.params(":code"));
res.header("Content-Type", "application/json");
} catch(Exception e) {
logger.error("cities", e);
LOGGER.error("cities", e);
res.status(500);
data = "ERROR";
}
@@ -115,11 +143,11 @@ public class Main {
String data;
try {
String query = "select uuid, name from cities where country_code = ? and city like ? order by name asc limit 10";
logger.info("Query " + query);
LOGGER.info("Query " + query);
data = queryToJson(query, req.params(":code"), req.params(":text") + "%");
res.header("Content-Type", "application/json");
} catch(Exception e) {
logger.error("match", e);
LOGGER.error("match", e);
res.status(500);
data = "ERROR";
}
@@ -145,7 +173,7 @@ public class Main {
data = new Gson().toJson(ship);
} else {
data = "no location";
logger.warn(data);
LOGGER.warn(data);
res.status(400);
}
@@ -153,9 +181,9 @@ public class Main {
});
Spark.post("/confirm/:id", (req, res) -> {
logger.info("confirm " + req.params(":id") + " - " + req.body());
String cart = addToCart(req.params(":id"), req.body());
logger.info("new cart " + cart);
LOGGER.info("confirm " + req.params(":id") + " - " + req.body());
String cart = addToCart(cartUrl, req.params(":id"), req.body());
LOGGER.info("new cart " + cart);
if(cart.equals("")) {
res.status(404);
@@ -166,18 +194,35 @@ public class Main {
return cart;
});
logger.info("Ready");
LOGGER.info("Ready");
}
private static String getJdbcURLFromVCAPServices(final JsonParser jsonParser) {
final JsonObject vcapServices = jsonParser.parse(System.getenv("VCAP_SERVICES")).getAsJsonObject();
for (Map.Entry<String, JsonElement> boundServices : vcapServices.entrySet()) {
final JsonArray bindings = boundServices.getValue().getAsJsonArray();
for (JsonElement boundService : bindings) {
final JsonObject bindingDetails = boundService.getAsJsonObject();
if (bindingDetails.has("binding_name") && "shipping_database".equals(bindingDetails.get("binding_name").getAsString())) {
final JsonObject credentials = bindingDetails.get("credentials").getAsJsonObject();
return credentials.get("jdbcUrl").getAsString();
}
}
}
throw new IllegalStateException("Cannot retrieve the 'shipping_database' service binding.");
}
/**
* Query to Json - QED
**/
private static String queryToJson(String query, Object ... args) {
List<Map<String, Object>> listOfMaps = null;
List<Map<String, Object>> listOfMaps;
try {
QueryRunner queryRunner = new QueryRunner(cpds);
QueryRunner queryRunner = new QueryRunner(CPDS);
listOfMaps = queryRunner.query(query, new MapListHandler(), args);
} catch (SQLException se) {
throw new RuntimeException("Couldn't query the database.", se);
@@ -197,7 +242,7 @@ public class Main {
String query = "select latitude, longitude from cities where uuid = ?";
try {
conn = cpds.getConnection();
conn = CPDS.getConnection();
stmt = conn.prepareStatement(query);
stmt.setInt(1, Integer.parseInt(uuid));
rs = stmt.executeQuery();
@@ -206,7 +251,7 @@ public class Main {
break;
}
} catch(Exception e) {
logger.error("Location exception", e);
LOGGER.error("Location exception", e);
} finally {
DbUtils.closeQuietly(conn, stmt, rs);
}
@@ -214,7 +259,7 @@ public class Main {
return location;
}
private static String addToCart(String id, String data) {
private static String addToCart(String cartUrl, String id, String data) {
StringBuilder buffer = new StringBuilder();
DefaultHttpClient httpClient = null;
@@ -224,7 +269,7 @@ public class Main {
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
httpClient = new DefaultHttpClient(httpParams);
HttpPost postRequest = new HttpPost(CART_URL + id);
HttpPost postRequest = new HttpPost(cartUrl + id);
StringEntity payload = new StringEntity(data);
payload.setContentType("application/json");
postRequest.setEntity(payload);
@@ -237,10 +282,10 @@ public class Main {
buffer.append(line);
}
} else {
logger.warn("Failed with code: " + res.getStatusLine().getStatusCode());
LOGGER.warn("Failed with code: " + res.getStatusLine().getStatusCode());
}
} catch(Exception e) {
logger.error("http client exception", e);
LOGGER.error("http client exception", e);
} finally {
if(httpClient != null) {
httpClient.getConnectionManager().shutdown();

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
#Generated by Maven
#Mon Aug 12 12:33:39 CEST 2019
groupId=steveww
artifactId=shipping
version=1.0

View File

@@ -0,0 +1,3 @@
org/steveww/spark/Ship.class
org/steveww/spark/Main.class
org/steveww/spark/Location.class

View File

@@ -0,0 +1,3 @@
/Users/michele/git/instana/robot-shop/shipping/src/main/java/org/steveww/spark/Ship.java
/Users/michele/git/instana/robot-shop/shipping/src/main/java/org/steveww/spark/Main.java
/Users/michele/git/instana/robot-shop/shipping/src/main/java/org/steveww/spark/Location.java

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,6 @@
const instana = require('instana-nodejs-sensor');
/*jshint esversion: 6 */
const instana = require('@instana/collector');
// init tracing
// MUST be done before loading anything else!
instana({
@@ -8,7 +10,6 @@ instana({
});
const mongoClient = require('mongodb').MongoClient;
const mongoObjectID = require('mongodb').ObjectID;
const redis = require('redis');
const bodyParser = require('body-parser');
const express = require('express');
@@ -241,9 +242,62 @@ app.get('/history/:id', (req, res) => {
}
});
var redisHost;
var redisPassword;
var redisPort;
if (process.env.VCAP_SERVICES) {
connectionDetails = null;
console.log('Env var \'VCAP_SERVICES\' found, scanning for \'users_cache\' service binding');
for (let [key, value] of Object.entries(JSON.parse(process.env.VCAP_SERVICES))) {
try {
binding = value.find(function(binding) {
return 'users_cache' == binding.binding_name && binding.credentials;
});
if (!binding) {
continue;
}
connectionDetails = binding.credentials;
if (connectionDetails) {
redisHost = connectionDetails.host;
redisPassword = connectionDetails.password;
redisPort = connectionDetails.port;
console.log('Redis URI for \'users_cache\' service binding found in \'VCAP_SERVICES\'');
break;
}
} catch (err) {
console.log('Cannot process key \'' + key + '\' of \'VCAP_SERVICES\'', err);
throw err;
}
}
} else if (process.env.REDIS_HOST) {
redisHost = process.env.REDIS_HOST;
redisPort = 6379;
console.log('Redis host found in \'REDIS_HOST\': ' + redisHost);
} else {
redisHost = 'redis';
redisPort = 6379;
console.log('Using default Redis host and port');
}
if (!redisHost) {
throw new Error('Redis connection data missing');
}
// connect to Redis
var redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'redis'
host: redisHost,
password: redisPassword,
port: redisPort
});
redisClient.on('error', (e) => {
@@ -256,15 +310,66 @@ redisClient.on('ready', (r) => {
// set up Mongo
function mongoConnect() {
return new Promise((resolve, reject) => {
var mongoURL = process.env.MONGO_URL || 'mongodb://mongodb:27017/users';
mongoClient.connect(mongoURL, (error, _db) => {
if(error) {
reject(error);
} else {
db = _db;
var mongoURL;
if (process.env.VCAP_SERVICES) {
connectionDetails = null;
console.log('Env var \'VCAP_SERVICES\' found, scanning for \'users_database\' service binding');
for (let [key, value] of Object.entries(JSON.parse(process.env.VCAP_SERVICES))) {
try {
binding = value.find(function(binding) {
return 'users_database' == binding.binding_name && binding.credentials;
});
if (!binding) {
continue;
}
connectionDetails = binding.credentials;
if (connectionDetails && connectionDetails.uri) {
mongoURL = connectionDetails.uri;
console.log('MongoDB URI for \'users_database\' service binding found in \'VCAP_SERVICES\'');
break;
} else {
throw new Error('Service binding \'users_database\' found, but cannot retrieve the URI from the credentials');
}
} catch (err) {
console.log('Cannot process key \'' + key + '\' of \'VCAP_SERVICES\'', err);
throw err;
}
}
} else if (process.env.MONGO_URL) {
mongoURL = process.env.MONGO_URL;
console.log('MongoDB URI found in \'MONGO_URL\': ' + mongoURL);
} else {
mongoURL = 'mongodb://mongodb:27017/catalogue';
console.log('Using default MongoDB URI');
}
if (!mongoURL) {
throw new Error('MongoDB connection data missing');
}
mongoClient.connect(mongoURL, (error, db) => {
if (error) {
throw error;
}
try {
usersCollection = db.collection('users');
ordersCollection = db.collection('orders');
resolve('connected');
} catch (err) {
console.log('Cannot connecto to MongoDB databases', err);
reject(err);
}
});
});

3
web/buildpack.yml Normal file
View File

@@ -0,0 +1,3 @@
---
nginx:
version: 1.17.2

View File

@@ -0,0 +1,5 @@
{
"service": "web",
"agent_host": "instana-agent",
"agent_port": 42699
}

Binary file not shown.

78
web/mime.types Normal file
View File

@@ -0,0 +1,78 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/x-javascript js;
application/atom+xml atom;
application/rss+xml rss;
font/ttf ttf;
font/woff woff;
font/woff2 woff2;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
text/cache-manifest manifest;
image/png png;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
image/svg+xml svg svgz;
image/webp webp;
application/java-archive jar war ear;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.ms-excel xls;
application/vnd.ms-powerpoint ppt;
application/vnd.wap.wmlc wmlc;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream eot;
application/octet-stream iso img;
application/octet-stream msi msp msm;
application/json json;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

Binary file not shown.

73
web/nginx.conf Normal file
View File

@@ -0,0 +1,73 @@
load_module modules/ngx_http_module.so;
worker_processes 1;
daemon off;
error_log stderr;
events {
worker_connections 1024;
}
pid /tmp/nginx.pid;
http {
opentracing on;
opentracing_load_tracer instana/libinstana_sensor.so instana/instana-config.json;
charset utf-8;
log_format cloudfoundry 'NginxLog "$request" $status $body_bytes_sent';
access_log /dev/stdout cloudfoundry;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
keepalive_timeout 30;
port_in_redirect off;
# Calculate CF app names reusing the CF apps domain as seen by the client
map $server_name $domain {
~^[^\.]*(.*)$ $1;
}
server {
listen {{port}};
server_name ~^(?<name>\w+)\.(?<domain>.*)$;
error_page 500 502 503 504 /50x.html;
# location = /50x.html {
# root /usr/share/nginx/html;
# }
location ~ ^/api/([a-zA-Z]+)/(.+)$ {
# We need a resolver, since we need to use variables, so we need to specify a DNS server.
# Google, HALP!
resolver 8.8.8.8;
proxy_pass https://$1.$domain/$2;
opentracing_trace_locations off;
opentracing_propagate_context;
opentracing_tag "resource.name" $1/$2;
opentracing_tag "endpoint" $1;
}
location /nginx_status {
stub_status on;
access_log off;
}
location / {
root static;
index index.html;
ssi on;
include mime.types;
opentracing_trace_locations off;
opentracing_propagate_context;
opentracing_tag "resource.name" "static_resources";
}
}
}

View File

@@ -1,11 +0,0 @@
<!-- EUM include -->
<script>
(function(i,s,o,g,r,a,m){i['InstanaEumObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//eum.instana.io/eum.min.js','ineum');
ineum('apiKey', 'INSTANA_EUM_KEY');
ineum('page', 'Shop');
</script>
<!-- EUM include end -->

10
web/static/eum.html Normal file
View File

@@ -0,0 +1,10 @@
<script>
(function(c,e,f,k,g,h,b,a,d){c[g]||(c[g]=h,b=c[h]=function(){
b.q.push(arguments)},b.q=[],b.l=1*new Date,a=e.createElement(f),a.async=1,
a.src=k,a.setAttribute("crossorigin", "anonymous"),d=e.getElementsByTagName(f)[0],
d.parentNode.insertBefore(a,d))})(window,document,"script",
"//eum.instana.io/eum.min.js","InstanaEumObject","ineum");
ineum('reportingUrl', 'TODO EDIT ME');
ineum('key', 'TODO EDIT ME');
ineum('page', 'RobotShop');
</script>

View File

@@ -1,7 +1,9 @@
/*jshint esversion: 6 */
(function(angular) {
'use strict';
var robotshop = angular.module('robotshop', ['ngRoute'])
var robotshop = angular.module('robotshop', ['ngRoute']);
// Share user between controllers
robotshop.factory('currentUser', function() {
@@ -211,10 +213,10 @@
$scope.rateProduct = function(score) {
console.log('rate product', $scope.data.product.sku, score);
var url = '/api/ratings/api/rate/' + $scope.data.product.sku + '/' + score;
var url = '/api/ratings/rate/' + $scope.data.product.sku + '/' + score;
$http({
url: url,
method: 'PUT'
method: 'POST'
}).then((res) => {
$scope.data.message = 'Thankyou for your feedback';
$timeout(clearMessage, 3000);
@@ -246,7 +248,7 @@
function loadRating(sku) {
$http({
url: '/api/ratings/api/fetch/' + sku,
url: '/api/ratings/fetch/' + sku,
method: 'GET'
}).then((res) => {
$scope.data.rating = res.data;