Compare commits
117 Commits
ecs
...
php_rating
Author | SHA1 | Date | |
---|---|---|---|
|
5edc308df0 | ||
|
18b7dd75c8 | ||
|
740d73549d | ||
|
ec5babff6e | ||
|
5fa1c41698 | ||
|
ea7f80d398 | ||
|
cbe067283c | ||
|
ea6f4c8c10 | ||
|
f774d0d1fd | ||
|
c6caf5964b | ||
|
e4ec129e4f | ||
|
b1adf4c650 | ||
|
48eeb4cc68 | ||
|
31b808e743 | ||
|
7316800e1f | ||
|
b5495f847c | ||
|
0328084ad4 | ||
|
9d8c34027f | ||
|
53a67a13cb | ||
|
d71c1216ab | ||
|
e80da6d5e0 | ||
|
b2384ee732 | ||
|
165e292328 | ||
|
f1becd3bd0 | ||
|
eaa36113a0 | ||
|
33a55a08e0 | ||
|
4c5903048c | ||
|
aa353caa67 | ||
|
0b45e47074 | ||
|
9581ee7646 | ||
|
0efbf5ead4 | ||
|
ccd7b31a7a | ||
|
fbd4024d10 | ||
|
5a22b100b2 | ||
|
4c03be5d1e | ||
|
bdc88e6e91 | ||
|
e90ebb9e33 | ||
|
ca390c6858 | ||
|
d1dd589672 | ||
|
2cf169500b | ||
|
905d185445 | ||
|
fa602802fa | ||
|
21afad2b70 | ||
|
71ddff31bc | ||
|
2fcc0c9835 | ||
|
db45fae8d6 | ||
|
f07213185f | ||
|
a8f89df7dd | ||
|
436e7eef7e | ||
|
9091749a9d | ||
|
d866400e91 | ||
|
65be51cb2b | ||
|
80616f0090 | ||
|
d60dc128c4 | ||
|
e181d9bf07 | ||
|
4c243455b8 | ||
|
8cd5551984 | ||
|
d45b613b61 | ||
|
a5ed9ab94e | ||
|
80082a58c0 | ||
|
4745f2393c | ||
|
a11b90275c | ||
|
0ceed6d94f | ||
|
29554c3023 | ||
|
d7a782b255 | ||
|
d98d955fae | ||
|
83ab82407c | ||
|
f0619949bc | ||
|
3d9ce2607b | ||
|
838f96536c | ||
|
e727811823 | ||
|
345ba7561c | ||
|
775e380318 | ||
|
5ef8c91c3c | ||
|
f0b49233a3 | ||
|
86bcdd57c2 | ||
|
247fbb02df | ||
|
7c3ffda0cb | ||
|
f0043a520b | ||
|
caf9f7b9dd | ||
|
c47eb4a1db | ||
|
f67e3f6f87 | ||
|
090851e57a | ||
|
c043fd38af | ||
|
1d5d632dd8 | ||
|
7e6b390dd9 | ||
|
1ab2ee68af | ||
|
4242f59c3f | ||
|
25bcebfc59 | ||
|
184093b14b | ||
|
6af5a3bcf4 | ||
|
8ea413da09 | ||
|
09b571c87e | ||
|
f06a04ff86 | ||
|
4063325ac1 | ||
|
ac8753e19e | ||
|
6bcb2872c3 | ||
|
83e1977b2f | ||
|
9369788ced | ||
|
f98c59a891 | ||
|
fcb1643bef | ||
|
1f6f0bd745 | ||
|
2fa53fc11e | ||
|
cf0ada6d51 | ||
|
a2e783d333 | ||
|
136dd5f987 | ||
|
c2b9581795 | ||
|
1f1c7719c1 | ||
|
15b13c69f9 | ||
|
4b201e58c3 | ||
|
661111b932 | ||
|
f7e9349742 | ||
|
c7b0f2a98d | ||
|
9ecba3f164 | ||
|
51f5ea6ecf | ||
|
50d4ab5406 | ||
|
37c4a2a9c3 |
2
.env
2
.env
@@ -1,3 +1,3 @@
|
||||
# environment file for docker-compose
|
||||
REPO=robotshop
|
||||
TAG=0.4.17
|
||||
TAG=2.1.0
|
||||
|
19
.github/workflows/push.yml
vendored
Normal file
19
.github/workflows/push.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: docker-compose-actions-workflow
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
paths-ignore:
|
||||
- 'DCOS/**'
|
||||
- 'K8s/**'
|
||||
- 'load-gen/**'
|
||||
- 'OpenShift/**'
|
||||
- 'Swarm/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build the stack
|
||||
run: docker-compose build
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -2,4 +2,9 @@
|
||||
.DS_Store
|
||||
*-private.*
|
||||
vendor/
|
||||
Gopkg.lock
|
||||
Gopkg.lock
|
||||
load-gen/utilities/__pycache__
|
||||
load-gen/logs/*.log
|
||||
load-gen/logs/*.log.[0-9]*
|
||||
load-gen/logs/*.csv
|
||||
agent/logs
|
||||
|
@@ -49,9 +49,9 @@ By default the `payment` service uses https://www.paypal.com as the pseudo payme
|
||||
$ helm install --set payment.gateway=https://foobar.com ...
|
||||
```
|
||||
|
||||
## End User Monitoring
|
||||
## Website Monitoring / End-User Monitoring
|
||||
|
||||
Optionally End User Monitoring can be enabled for the web pages. Take a look at the [documentation](https://docs.instana.io/products/website_monitoring/) to see how to get a key and an endpoint url.
|
||||
Optionally Website Monitoring / End-User Monitoring can be enabled for the web pages. Take a look at the [documentation](https://docs.instana.io/website_monitoring/) to see how to get a key and an endpoint url.
|
||||
|
||||
```shell
|
||||
$ helm install \
|
||||
@@ -60,3 +60,38 @@ $ helm install \
|
||||
...
|
||||
```
|
||||
|
||||
## Use with Minis
|
||||
|
||||
When running on `minishift` or `minikube` set `nodeport` to true. The store will then be available on the IP address of your mini and node port of the web service.
|
||||
|
||||
```shell
|
||||
$ mini[kube|shift] ip
|
||||
192.168.66.101
|
||||
$ kubectl get svc web
|
||||
```
|
||||
|
||||
Combine the IP and port number to make the URL `http://192.168.66.101:32145`
|
||||
|
||||
### MiniShift
|
||||
|
||||
Openshift is like K8s but not K8s. Set `openshift` to true or things will break. See the notes and scripts in the OpenShift directory of this repo.
|
||||
|
||||
```shell
|
||||
$ helm install robot-shop --set openshift=true helm
|
||||
```
|
||||
|
||||
### Deployment Parameters
|
||||
|
||||
| Key | Default | Type | Description |
|
||||
| ---------------- | ------- | ------ | ----------- |
|
||||
| eum.key | null | string | EUM Access Key |
|
||||
| eum.url | https://eum-eu-west-1.instana.io | url | EUM endpoint URL |
|
||||
| image.pullPolicy | IfNotPresent | string | Kubernetes pull policy. One of Always,IfNotPresent, or Never. |
|
||||
| image.repo | robotshop | string | Base docker repository to pull the images from. |
|
||||
| image.version | latest | string | Docker tag to pull. |
|
||||
| nodeport | false | booelan | Whether to expose the services via node port. |
|
||||
| openshift | false | boolean | If OpenShift additional configuration is applied. |
|
||||
| payment.gateway | null | string | External URL end-point to simulate partial/3rd party traces. |
|
||||
| psp.enabled | false | boolean | Enable Pod Security Policy for clusters with a PSP Admission controller |
|
||||
| redis.storageClassName | standard | string | Storage class to use with Redis's StatefulSet. The default for EKS is gp2. |
|
||||
| ocCreateRoute | false | boolean | If you are running on OpenShift and need a Route to the web service, set this to `true` |
|
||||
|
@@ -20,7 +20,8 @@ spec:
|
||||
containers:
|
||||
- name: cart
|
||||
image: {{ .Values.image.repo }}/rs-cart:{{ .Values.image.version }}
|
||||
# agent networking access
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# agent networking access
|
||||
env:
|
||||
- name: INSTANA_AGENT_HOST
|
||||
valueFrom:
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: catalogue
|
||||
image: {{ .Values.image.repo }}/rs-catalogue:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: INSTANA_AGENT_HOST
|
||||
valueFrom:
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: dispatch
|
||||
image: {{ .Values.image.repo }}/rs-dispatch:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
# agent networking access
|
||||
- name: INSTANA_AGENT_HOST
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: {{ .Values.image.repo }}/rs-mongodb:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 27017
|
||||
resources:
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: mysql
|
||||
image: {{ .Values.image.repo }}/rs-mysql-db:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# added for Istio
|
||||
securityContext:
|
||||
capabilities:
|
||||
|
@@ -23,6 +23,7 @@ spec:
|
||||
containers:
|
||||
- name: payment
|
||||
image: {{ .Values.image.repo }}/rs-payment:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# agent networking access
|
||||
env:
|
||||
- name: INSTANA_AGENT_HOST
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: rabbitmq
|
||||
image: rabbitmq:3.7-management-alpine
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 5672
|
||||
- containerPort: 15672
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: ratings
|
||||
image: {{ .Values.image.repo }}/rs-ratings:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
@@ -29,4 +30,12 @@ spec:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 50Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /_health
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
successThreshold: 1
|
||||
restartPolicy: Always
|
||||
|
@@ -1,5 +1,5 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
labels:
|
||||
service: redis
|
||||
@@ -9,6 +9,7 @@ spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
service: redis
|
||||
serviceName: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
@@ -20,8 +21,12 @@ spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:4.0.6
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /mnt/redis
|
||||
resources:
|
||||
limits:
|
||||
cpu: 200m
|
||||
@@ -30,3 +35,16 @@ spec:
|
||||
cpu: 100m
|
||||
memory: 50Mi
|
||||
restartPolicy: Always
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes: [ "ReadWriteOnce" ]
|
||||
{{ if not .Values.openshift }}
|
||||
storageClassName: {{ .Values.redis.storageClassName }}
|
||||
volumeMode: Filesystem
|
||||
{{ end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: shipping
|
||||
image: {{ .Values.image.repo }}/rs-shipping:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
# it's Java it needs lots of memory
|
||||
@@ -30,4 +31,12 @@ spec:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 500Mi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 30
|
||||
successThreshold: 1
|
||||
restartPolicy: Always
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: user
|
||||
image: {{ .Values.image.repo }}/rs-user:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
# agent networking access
|
||||
- name: INSTANA_AGENT_HOST
|
||||
|
@@ -20,6 +20,7 @@ spec:
|
||||
containers:
|
||||
- name: web
|
||||
image: {{ .Values.image.repo }}/rs-web:{{ .Values.image.version }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
{{- if .Values.eum.key }}
|
||||
env:
|
||||
- name: INSTANA_EUM_KEY
|
||||
|
@@ -11,4 +11,19 @@ spec:
|
||||
targetPort: 8080
|
||||
selector:
|
||||
service: web
|
||||
{{ if .Values.nodeport }}
|
||||
type: NodePort
|
||||
{{ else }}
|
||||
type: LoadBalancer
|
||||
{{ end }}
|
||||
---
|
||||
{{if .Values.ocCreateRoute}}
|
||||
apiVersion: route.openshift.io/v1
|
||||
kind: Route
|
||||
metadata:
|
||||
name: web
|
||||
spec:
|
||||
to:
|
||||
kind: Service
|
||||
name: web
|
||||
{{end}}
|
@@ -3,6 +3,7 @@
|
||||
image:
|
||||
repo: robotshop
|
||||
version: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Alternative payment gateway URL
|
||||
# Default is https://www.paypal.com
|
||||
@@ -21,3 +22,14 @@ eum:
|
||||
psp:
|
||||
enabled: false
|
||||
|
||||
# For the mini ones minikube, minishift set to true
|
||||
nodeport: false
|
||||
|
||||
# "special" Openshift. Set to true when deploying to any openshift flavour
|
||||
openshift: false
|
||||
|
||||
# Storage class to use with redis statefulset.
|
||||
redis:
|
||||
storageClassName: standard
|
||||
|
||||
ocCreateRoute: false
|
||||
|
@@ -4,17 +4,45 @@ See the official [documentation](https://docs.instana.io/quick_start/agent_setup
|
||||
|
||||
# Robot Shop Deployment
|
||||
|
||||
Have a look at the contents of the *setup.sh* and *deploy,sh* scripts, you may want to tweak some settings to suit your environment.
|
||||
## OCP 3.x
|
||||
|
||||
Run the *setup.sh* script first, you will need the passwords for the developer and system:admin users.
|
||||
For OpenShift run the `setup.sh` script to create the project and set the extra permissions.
|
||||
|
||||
Once the set up is completed, run the *deploy.sh* script. This script imports the application images from Docker Hub into OpenShift, then it creates applications from those images.
|
||||
Use the Helm chart for Kubernetes to install Stan's Robot Shop. To install on Minishift.
|
||||
|
||||
When the deployment has completed, to make Stan's Robot Shop accessible the web service needs to be updated.
|
||||
### Helm 3
|
||||
|
||||
```bash
|
||||
oc edit svc web
|
||||
```shell
|
||||
$ cd K8s
|
||||
$ oc login -u developer
|
||||
$ oc project robot-shop
|
||||
$ helm install robot-shop --set openshift=true --set nodeport=true helm
|
||||
```
|
||||
|
||||
Change *type* to **NodePort** when running on Minishift or **LoadBalancer** for regular OpenShift.
|
||||
To connect to the shop.
|
||||
|
||||
```shell
|
||||
$ minishift ip
|
||||
192.168.99.106
|
||||
$ oc get svc web
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
web NodePort 172.30.180.253 <none> 8080:31147/TCP 4m
|
||||
```
|
||||
|
||||
Use the IP and the node port to form the URL `http://192.168.99.106:31147/`
|
||||
|
||||
## OCP 4.x
|
||||
|
||||
For Openshift cluster in version 4.x follow these steps:
|
||||
|
||||
```
|
||||
export KUBECONFIG=/path/to/oc/cluster/dir/auth/kubeconfig
|
||||
oc adm new-project robot-shop
|
||||
oc adm policy add-scc-to-user anyuid -z default -n robot-shop
|
||||
oc adm policy add-scc-to-user privileged -z default -n robot-shop
|
||||
cd robot-shop/K8s
|
||||
helm install robot-shop --set openshift=true -n robot-shop helm
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
@@ -1,48 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# set -x
|
||||
|
||||
set -e
|
||||
|
||||
# Put your EUM key here
|
||||
EUM_KEY=""
|
||||
|
||||
|
||||
echo "logging in as developer"
|
||||
oc login -u developer
|
||||
oc project robot-shop
|
||||
|
||||
# set the environment from the .env file
|
||||
for VAR in $(egrep '^[A-Z]+=' ../.env)
|
||||
do
|
||||
export $VAR
|
||||
done
|
||||
|
||||
# import all the images from docker hub into OpenShift
|
||||
for LINE in $(awk '/^ {2}[a-z]+:$/ {printf "%s", $0} /image: / {print $2}' ../docker-compose.yaml)
|
||||
do
|
||||
NAME=$(echo "$LINE" | cut -d: -f1)
|
||||
IMAGE=$(echo "$LINE" | cut -d: -f2-)
|
||||
FULL_IMAGE=$(eval "echo $IMAGE")
|
||||
|
||||
echo "NAME $NAME"
|
||||
echo "importing $FULL_IMAGE"
|
||||
|
||||
oc import-image $FULL_IMAGE --from $FULL_IMAGE --confirm
|
||||
# a bit of a hack but appears to work
|
||||
BASE=$(basename $FULL_IMAGE)
|
||||
oc new-app -i $BASE --name $NAME
|
||||
done
|
||||
|
||||
# Set EUM environment if required
|
||||
if [ -n "$EUM_KEY" ]
|
||||
then
|
||||
oc set env dc/web INSTANA_EUM_KEY=$EUM_KEY
|
||||
fi
|
||||
|
||||
echo " "
|
||||
echo "Deployment complete"
|
||||
echo "To make Robot Shop accessible, please run <oc edit svc web>"
|
||||
echo "Change type from ClusterIP to NodePort on minishift or LoadBalancer on OpenShift"
|
||||
echo " "
|
||||
|
@@ -1,28 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Put your EUM key here
|
||||
EUM_KEY=""
|
||||
|
||||
# set -x
|
||||
|
||||
# This only works for default local install of minishift
|
||||
# Need to tweak some settings in OpenShift
|
||||
echo "logging in as system:admin"
|
||||
oc login -u system:admin
|
||||
|
||||
# Optionally label the nodes with role infra
|
||||
for NODE in $(oc get node | awk '{if ($3 == "infra" || $3 == "<none>") print $1}' -)
|
||||
do
|
||||
oc label node $NODE 'type=infra'
|
||||
done
|
||||
|
||||
oc adm new-project robot-shop --node-selector='type=infra'
|
||||
oc adm new-project robot-shop
|
||||
oc adm policy add-role-to-user admin developer -n robot-shop
|
||||
oc adm policy add-scc-to-user anyuid -z default
|
||||
oc adm policy add-scc-to-user privileged -z default
|
||||
|
||||
oc logout
|
||||
|
||||
echo " "
|
||||
echo "OpenShift set up complete, ready to deploy Robot Shop now."
|
||||
echo " "
|
||||
oc login -u developer
|
||||
|
||||
|
66
README.md
66
README.md
@@ -6,7 +6,7 @@ You can get more detailed information from my [blog post](https://www.instana.co
|
||||
|
||||
This sample microservice application has been built using these technologies:
|
||||
- NodeJS ([Express](http://expressjs.com/))
|
||||
- Java ([Spark Java](http://sparkjava.com/))
|
||||
- Java ([Spring Boot](https://spring.io/))
|
||||
- Python ([Flask](http://flask.pocoo.org))
|
||||
- Golang
|
||||
- PHP (Apache)
|
||||
@@ -24,6 +24,14 @@ To see the application performance results in the Instana dashboard, you will fi
|
||||
## Build from Source
|
||||
To optionally build from source (you will need a newish version of Docker to do this) use Docker Compose. Optionally edit the `.env` file to specify an alternative image registry and version tag; see the official [documentation](https://docs.docker.com/compose/env-file/) for more information.
|
||||
|
||||
To download the tracing module for Nginx, it needs a valid Instana agent key. Set this in the environment before starting the build.
|
||||
|
||||
```shell
|
||||
$ export INSTANA_AGENT_KEY="<your agent key>"
|
||||
```
|
||||
|
||||
Now build all the images.
|
||||
|
||||
```shell
|
||||
$ docker-compose build
|
||||
```
|
||||
@@ -70,57 +78,18 @@ You can run Kubernetes locally using [minikube](https://github.com/kubernetes/mi
|
||||
|
||||
The Docker container images are all available on [Docker Hub](https://hub.docker.com/u/robotshop/).
|
||||
|
||||
Install Stan's Robot Shop to your Kubernetes cluster using the helm chart.
|
||||
Install Stan's Robot Shop to your Kubernetes cluster using the [Helm](K8s/helm/README.md) chart.
|
||||
|
||||
```shell
|
||||
$ cd K8s/helm
|
||||
$ helm install --name robot-shop --namespace robot-shop .
|
||||
```
|
||||
|
||||
There are some customisations that can be made see the [README](K8s/helm/README.md).
|
||||
|
||||
To deploy the Instana agent to Kubernetes, just use the [helm](https://hub.helm.sh/charts/stable/instana-agent) chart.
|
||||
|
||||
```shell
|
||||
$ helm install --name instana-agent --namespace instana-agent \
|
||||
--set agent.key=INSTANA_AGENT_KEY \
|
||||
--set agent.endpointHost=HOST \
|
||||
--set agent.endpointPort=PORT \
|
||||
--set zone.name=CLUSTER_NAME \
|
||||
stable/instana-agent
|
||||
```
|
||||
|
||||
If you are having difficulties getting 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.
|
||||
To deploy the Instana agent to Kubernetes, just use the [helm](https://github.com/instana/helm-charts) chart.
|
||||
|
||||
## 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*.
|
||||
|
||||
```shell
|
||||
$ kubectl -n robot-shop edit service web
|
||||
```
|
||||
|
||||
Snippet
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
ports:
|
||||
- name: "8080"
|
||||
port: 8080
|
||||
protocol: TCP
|
||||
targetPort: 8080
|
||||
nodePort: 30080
|
||||
selector:
|
||||
service: web
|
||||
sessionAffinity: None
|
||||
type: NodePort
|
||||
```
|
||||
|
||||
The store front is then available on the IP address of minikube port 30080. To find the IP address of your minikube instance.
|
||||
If you are running the store on Kubernetes via minikube then, find the IP address of Minikube and the Node Port of the web service.
|
||||
|
||||
```shell
|
||||
$ minikube ip
|
||||
$ kubectl get svc web
|
||||
```
|
||||
|
||||
If you are using a cloud Kubernetes / Openshift / Mesosphere then it will be available on the load balancer of that system.
|
||||
@@ -128,16 +97,15 @@ If you are using a cloud Kubernetes / Openshift / Mesosphere then it will be ava
|
||||
## Load Generation
|
||||
A separate load generation utility is provided in the `load-gen` directory. This is not automatically run when the application is started. The load generator is built with Python and [Locust](https://locust.io). The `build.sh` script builds the Docker image, optionally taking *push* as the first argument to also push the image to the registry. The registry and tag settings are loaded from the `.env` file in the parent directory. The script `load-gen.sh` runs the image, it takes a number of command line arguments. You could run the container inside an orchestration system (K8s) as well if you want to, an example descriptor is provided in K8s directory. For more details see the [README](load-gen/README.md) in the load-gen directory.
|
||||
|
||||
## End User Monitoring
|
||||
## Website Monitoring / End-User Monitoring
|
||||
|
||||
### Docker Compose
|
||||
|
||||
To enable End User Monitoring (EUM) see the official [documentation](https://docs.instana.io/products/website_monitoring/) for how to create a configuration. There is no need to inject the javascript fragment into the page, this will be handled automatically. Just make a note of the unique key and set the environment variable INSTANA_EUM_KEY for the web image, see `docker-compose.yaml` for an example.
|
||||
|
||||
If you are running the Instana backend on premise, you will also need to set the Reporting URL to your local instance. Set the environment variable INSTANA_EUM_REPORTING_URL as above. See the Instana EUM API [reference](https://docs.instana.io/products/website_monitoring/api/#api-structure)
|
||||
To enable Website Monioring / End-User Monitoring (EUM) see the official [documentation](https://docs.instana.io/website_monitoring/) for how to create a configuration. There is no need to inject the JavaScript fragment into the page, this will be handled automatically. Just make a note of the unique key and set the environment variable `INSTANA_EUM_KEY` and `INSTANA_EUM_REPORTING_URL` for the web image within `docker-compose.yaml`.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
The Helm chart for installing Stan's Robot Shop supports setting the key and endpoint url for EUM, see the [README](K8s/helm/README.md).
|
||||
The Helm chart for installing Stan's Robot Shop supports setting the key and endpoint url required for website monitoring, see the [README](K8s/helm/README.md).
|
||||
|
||||
## Prometheus
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
FROM node:10
|
||||
FROM node:14
|
||||
|
||||
ENV INSTANA_AUTO_PROFILE true
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
@@ -12,11 +12,11 @@
|
||||
"body-parser": "^1.18.1",
|
||||
"express": "^4.15.4",
|
||||
"redis": "^2.8.0",
|
||||
"request": "^2.83.0",
|
||||
"request": "^2.88.2",
|
||||
"pino": "^5.10.8",
|
||||
"express-pino-logger": "^4.0.0",
|
||||
"pino-pretty": "^2.5.0",
|
||||
"@instana/collector": "^1.65.0",
|
||||
"@instana/collector": "^1.132.2",
|
||||
"prom-client": "^11.5.3"
|
||||
}
|
||||
}
|
||||
|
@@ -48,6 +48,20 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
let dcs = [
|
||||
"asia-northeast2",
|
||||
"asia-south1",
|
||||
"europe-west3",
|
||||
"us-east1",
|
||||
"us-west1"
|
||||
];
|
||||
let span = instana.currentSpan();
|
||||
span.annotate('custom.sdk.tags.datacenter', dcs[Math.floor(Math.random() * dcs.length)]);
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
|
@@ -1,4 +1,6 @@
|
||||
FROM node:10
|
||||
FROM node:14
|
||||
|
||||
ENV INSTANA_AUTO_PROFILE true
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
@@ -15,6 +15,6 @@
|
||||
"pino": "^5.10.8",
|
||||
"express-pino-logger": "^4.0.0",
|
||||
"pino-pretty": "^2.5.0",
|
||||
"@instana/collector": "^1.90.0"
|
||||
"@instana/collector": "^1.132.2"
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,20 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
let dcs = [
|
||||
"asia-northeast2",
|
||||
"asia-south1",
|
||||
"europe-west3",
|
||||
"us-east1",
|
||||
"us-west1"
|
||||
];
|
||||
let span = instana.currentSpan();
|
||||
span.annotate('custom.sdk.tags.datacenter', dcs[Math.floor(Math.random() * dcs.length)]);
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
|
@@ -1,16 +1,10 @@
|
||||
FROM golang:1.12.7
|
||||
FROM golang:1.17
|
||||
|
||||
ENV GOPATH=/go
|
||||
WORKDIR /go/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y go-dep
|
||||
COPY *.go .
|
||||
|
||||
WORKDIR /go/src/github.com/instana/dispatch
|
||||
RUN go mod init dispatch && go get
|
||||
RUN go install
|
||||
|
||||
COPY src/ /go/src/github.com/instana/dispatch
|
||||
|
||||
RUN dep init && dep ensure
|
||||
RUN go build -o bin/gorcv
|
||||
|
||||
# TODO stage this build
|
||||
|
||||
CMD bin/gorcv
|
||||
CMD dispatch
|
||||
|
233
dispatch/main.go
Normal file
233
dispatch/main.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/instana/go-sensor"
|
||||
ot "github.com/opentracing/opentracing-go"
|
||||
"github.com/opentracing/opentracing-go/ext"
|
||||
otlog "github.com/opentracing/opentracing-go/log"
|
||||
"github.com/streadway/amqp"
|
||||
)
|
||||
|
||||
const (
|
||||
Service = "dispatch"
|
||||
)
|
||||
|
||||
var (
|
||||
amqpUri string
|
||||
rabbitChan *amqp.Channel
|
||||
rabbitCloseError chan *amqp.Error
|
||||
rabbitReady chan bool
|
||||
errorPercent int
|
||||
|
||||
dataCenters = []string{
|
||||
"asia-northeast2",
|
||||
"asia-south1",
|
||||
"europe-west3",
|
||||
"us-east1",
|
||||
"us-west1",
|
||||
}
|
||||
)
|
||||
|
||||
func connectToRabbitMQ(uri string) *amqp.Connection {
|
||||
for {
|
||||
conn, err := amqp.Dial(uri)
|
||||
if err == nil {
|
||||
return conn
|
||||
}
|
||||
|
||||
log.Println(err)
|
||||
log.Printf("Reconnecting to %s\n", uri)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func rabbitConnector(uri string) {
|
||||
var rabbitErr *amqp.Error
|
||||
|
||||
for {
|
||||
rabbitErr = <-rabbitCloseError
|
||||
if rabbitErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Connecting to %s\n", amqpUri)
|
||||
rabbitConn := connectToRabbitMQ(uri)
|
||||
rabbitConn.NotifyClose(rabbitCloseError)
|
||||
|
||||
var err error
|
||||
|
||||
// create mappings here
|
||||
rabbitChan, err = rabbitConn.Channel()
|
||||
failOnError(err, "Failed to create channel")
|
||||
|
||||
// create exchange
|
||||
err = rabbitChan.ExchangeDeclare("robot-shop", "direct", true, false, false, false, nil)
|
||||
failOnError(err, "Failed to create exchange")
|
||||
|
||||
// create queue
|
||||
queue, err := rabbitChan.QueueDeclare("orders", true, false, false, false, nil)
|
||||
failOnError(err, "Failed to create queue")
|
||||
|
||||
// bind queue to exchange
|
||||
err = rabbitChan.QueueBind(queue.Name, "orders", "robot-shop", false, nil)
|
||||
failOnError(err, "Failed to bind queue")
|
||||
|
||||
// signal ready
|
||||
rabbitReady <- true
|
||||
}
|
||||
}
|
||||
|
||||
func failOnError(err error, msg string) {
|
||||
if err != nil {
|
||||
log.Fatalf("%s : %s", msg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getOrderId(order []byte) string {
|
||||
id := "unknown"
|
||||
var f interface{}
|
||||
err := json.Unmarshal(order, &f)
|
||||
if err == nil {
|
||||
m := f.(map[string]interface{})
|
||||
id = m["orderid"].(string)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func createSpan(headers map[string]interface{}, order string) {
|
||||
// headers is map[string]interface{}
|
||||
// carrier is map[string]string
|
||||
carrier := make(ot.TextMapCarrier)
|
||||
// convert by copying k, v
|
||||
for k, v := range headers {
|
||||
carrier[k] = v.(string)
|
||||
}
|
||||
|
||||
// get the order id
|
||||
log.Printf("order %s\n", order)
|
||||
|
||||
// opentracing
|
||||
var span ot.Span
|
||||
tracer := ot.GlobalTracer()
|
||||
spanContext, err := tracer.Extract(ot.HTTPHeaders, carrier)
|
||||
if err == nil {
|
||||
log.Println("Creating child span")
|
||||
// create child span
|
||||
span = tracer.StartSpan("getOrder", ot.ChildOf(spanContext))
|
||||
|
||||
fakeDataCenter := dataCenters[rand.Intn(len(dataCenters))]
|
||||
span.SetTag("datacenter", fakeDataCenter)
|
||||
} else {
|
||||
log.Println(err)
|
||||
log.Println("Failed to get context from headers")
|
||||
log.Println("Creating root span")
|
||||
// create root span
|
||||
span = tracer.StartSpan("getOrder")
|
||||
}
|
||||
|
||||
span.SetTag(string(ext.SpanKind), ext.SpanKindConsumerEnum)
|
||||
span.SetTag(string(ext.MessageBusDestination), "robot-shop")
|
||||
span.SetTag("exchange", "robot-shop")
|
||||
span.SetTag("sort", "consume")
|
||||
span.SetTag("address", "rabbitmq")
|
||||
span.SetTag("key", "orders")
|
||||
span.LogFields(otlog.String("orderid", order))
|
||||
defer span.Finish()
|
||||
|
||||
time.Sleep(time.Duration(42+rand.Int63n(42)) * time.Millisecond)
|
||||
if rand.Intn(100) < errorPercent {
|
||||
span.SetTag("error", true)
|
||||
span.LogFields(
|
||||
otlog.String("error.kind", "Exception"),
|
||||
otlog.String("message", "Failed to dispatch to SOP"))
|
||||
log.Println("Span tagged with error")
|
||||
}
|
||||
|
||||
processSale(span)
|
||||
}
|
||||
|
||||
func processSale(parentSpan ot.Span) {
|
||||
tracer := ot.GlobalTracer()
|
||||
span := tracer.StartSpan("processSale", ot.ChildOf(parentSpan.Context()))
|
||||
defer span.Finish()
|
||||
span.SetTag(string(ext.SpanKind), "intermediate")
|
||||
span.LogFields(otlog.String("info", "Order sent for processing"))
|
||||
time.Sleep(time.Duration(42+rand.Int63n(42)) * time.Millisecond)
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
// Instana tracing
|
||||
ot.InitGlobalTracer(instana.NewTracerWithOptions(&instana.Options{
|
||||
Service: Service,
|
||||
LogLevel: instana.Info,
|
||||
EnableAutoProfile: true,
|
||||
}))
|
||||
|
||||
// Init amqpUri
|
||||
// get host from environment
|
||||
amqpHost, ok := os.LookupEnv("AMQP_HOST")
|
||||
if !ok {
|
||||
amqpHost = "rabbitmq"
|
||||
}
|
||||
amqpUri = fmt.Sprintf("amqp://guest:guest@%s:5672/", amqpHost)
|
||||
|
||||
// get error threshold from environment
|
||||
errorPercent = 0
|
||||
epct, ok := os.LookupEnv("DISPATCH_ERROR_PERCENT")
|
||||
if ok {
|
||||
epcti, err := strconv.Atoi(epct)
|
||||
if err == nil {
|
||||
if epcti > 100 {
|
||||
epcti = 100
|
||||
}
|
||||
if epcti < 0 {
|
||||
epcti = 0
|
||||
}
|
||||
errorPercent = epcti
|
||||
}
|
||||
}
|
||||
log.Printf("Error Percent is %d\n", errorPercent)
|
||||
|
||||
// MQ error channel
|
||||
rabbitCloseError = make(chan *amqp.Error)
|
||||
|
||||
// MQ ready channel
|
||||
rabbitReady = make(chan bool)
|
||||
|
||||
go rabbitConnector(amqpUri)
|
||||
|
||||
rabbitCloseError <- amqp.ErrClosed
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// wait for rabbit to be ready
|
||||
ready := <-rabbitReady
|
||||
log.Printf("Rabbit MQ ready %v\n", ready)
|
||||
|
||||
// subscribe to bound queue
|
||||
msgs, err := rabbitChan.Consume("orders", "", true, false, false, false, nil)
|
||||
failOnError(err, "Failed to consume")
|
||||
|
||||
for d := range msgs {
|
||||
log.Printf("Order %s\n", d.Body)
|
||||
log.Printf("Headers %v\n", d.Headers)
|
||||
id := getOrderId(d.Body)
|
||||
go createSpan(d.Headers, id)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Waiting for messages")
|
||||
select {}
|
||||
}
|
@@ -1,219 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"os"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"encoding/json"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
Service = "dispatch"
|
||||
)
|
||||
|
||||
var (
|
||||
amqpUri string
|
||||
rabbitChan *amqp.Channel
|
||||
rabbitCloseError chan *amqp.Error
|
||||
rabbitReady chan bool
|
||||
errorPercent int
|
||||
)
|
||||
|
||||
func connectToRabbitMQ(uri string) *amqp.Connection {
|
||||
for {
|
||||
conn, err := amqp.Dial(uri)
|
||||
if err == nil {
|
||||
return conn
|
||||
}
|
||||
|
||||
log.Println(err)
|
||||
log.Printf("Reconnecting to %s\n", uri)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func rabbitConnector(uri string) {
|
||||
var rabbitErr *amqp.Error
|
||||
|
||||
for {
|
||||
rabbitErr = <-rabbitCloseError
|
||||
if rabbitErr != nil {
|
||||
log.Printf("Connecting to %s\n", amqpUri)
|
||||
rabbitConn := connectToRabbitMQ(uri)
|
||||
rabbitConn.NotifyClose(rabbitCloseError)
|
||||
|
||||
var err error
|
||||
|
||||
// create mappings here
|
||||
rabbitChan, err = rabbitConn.Channel()
|
||||
failOnError(err, "Failed to create channel")
|
||||
|
||||
// create exchange
|
||||
err = rabbitChan.ExchangeDeclare("robot-shop", "direct", true, false, false, false, nil)
|
||||
failOnError(err, "Failed to create exchange")
|
||||
|
||||
// create queue
|
||||
queue, err := rabbitChan.QueueDeclare("orders", true, false, false, false, nil)
|
||||
failOnError(err, "Failed to create queue")
|
||||
|
||||
// bind queue to exchange
|
||||
err = rabbitChan.QueueBind(queue.Name, "orders", "robot-shop", false, nil)
|
||||
failOnError(err, "Failed to bind queue")
|
||||
|
||||
// signal ready
|
||||
rabbitReady <- true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func failOnError(err error, msg string) {
|
||||
if err != nil {
|
||||
log.Fatalf("$s : %s", msg, err)
|
||||
panic(fmt.Sprintf("%s : %s", msg, err))
|
||||
}
|
||||
}
|
||||
|
||||
func getOrderId(order []byte) string {
|
||||
id := "unknown"
|
||||
var f interface{}
|
||||
err := json.Unmarshal(order, &f)
|
||||
if err == nil {
|
||||
m := f.(map[string]interface{})
|
||||
id = m["orderid"].(string)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func createSpan(headers map[string]interface{}, order string) {
|
||||
// headers is map[string]interface{}
|
||||
// carrier is map[string]string
|
||||
carrier := make(ot.TextMapCarrier)
|
||||
// convert by copying k, v
|
||||
for k, v := range headers {
|
||||
carrier[k] = v.(string)
|
||||
}
|
||||
|
||||
// get the order id
|
||||
log.Printf("order %s\n", order)
|
||||
|
||||
// opentracing
|
||||
var span ot.Span
|
||||
tracer := ot.GlobalTracer()
|
||||
spanContext, err := tracer.Extract(ot.HTTPHeaders, carrier)
|
||||
if err == nil {
|
||||
log.Println("Creating child span")
|
||||
// create child span
|
||||
span = tracer.StartSpan("getOrder", ot.ChildOf(spanContext))
|
||||
} else {
|
||||
log.Println(err)
|
||||
log.Println("Failed to get context from headers")
|
||||
log.Println("Creating root span")
|
||||
// create root span
|
||||
span = tracer.StartSpan("getOrder")
|
||||
}
|
||||
|
||||
span.SetTag(string(ext.SpanKind), ext.SpanKindConsumerEnum)
|
||||
span.SetTag(string(ext.MessageBusDestination), "robot-shop")
|
||||
span.SetTag("exchange", "robot-shop")
|
||||
span.SetTag("sort", "consume")
|
||||
span.SetTag("address", "rabbitmq")
|
||||
span.SetTag("key", "orders")
|
||||
span.LogFields(otlog.String("orderid", order))
|
||||
defer span.Finish()
|
||||
|
||||
time.Sleep(time.Duration(42 + rand.Int63n(42)) * time.Millisecond)
|
||||
if rand.Intn(100) < errorPercent {
|
||||
span.SetTag("error", true)
|
||||
span.LogFields(
|
||||
otlog.String("error.kind", "Exception"),
|
||||
otlog.String("message", "Failed to dispatch to SOP"))
|
||||
log.Println("Span tagged with error")
|
||||
}
|
||||
|
||||
processSale(span)
|
||||
}
|
||||
|
||||
func processSale(parentSpan ot.Span) {
|
||||
tracer := ot.GlobalTracer()
|
||||
span := tracer.StartSpan("processSale", ot.ChildOf(parentSpan.Context()))
|
||||
defer span.Finish()
|
||||
span.SetTag(string(ext.SpanKind), "intermediate")
|
||||
span.LogFields(otlog.String("info", "Order sent for processing"))
|
||||
time.Sleep(time.Duration(42 + rand.Int63n(42)) * time.Millisecond)
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
// Instana tracing
|
||||
ot.InitGlobalTracer(instana.NewTracerWithOptions(&instana.Options{
|
||||
Service: Service,
|
||||
LogLevel: instana.Info}))
|
||||
|
||||
// Init amqpUri
|
||||
// get host from environment
|
||||
amqpHost, ok := os.LookupEnv("AMQP_HOST")
|
||||
if !ok {
|
||||
amqpHost = "rabbitmq"
|
||||
}
|
||||
amqpUri = fmt.Sprintf("amqp://guest:guest@%s:5672/", amqpHost)
|
||||
|
||||
// get error threshold from environment
|
||||
errorPercent = 0
|
||||
epct, ok := os.LookupEnv("DISPATCH_ERROR_PERCENT")
|
||||
if ok {
|
||||
epcti, err := strconv.Atoi(epct)
|
||||
if err == nil {
|
||||
if epcti > 100 {
|
||||
epcti = 100
|
||||
}
|
||||
if epcti < 0 {
|
||||
epcti = 0
|
||||
}
|
||||
errorPercent = epcti
|
||||
}
|
||||
}
|
||||
log.Printf("Error Percent is %d\n", errorPercent)
|
||||
|
||||
// MQ error channel
|
||||
rabbitCloseError = make(chan *amqp.Error)
|
||||
|
||||
// MQ ready channel
|
||||
rabbitReady = make(chan bool)
|
||||
|
||||
go rabbitConnector(amqpUri)
|
||||
|
||||
rabbitCloseError <- amqp.ErrClosed
|
||||
|
||||
go func() {
|
||||
for {
|
||||
// wait for rabbit to be ready
|
||||
ready := <-rabbitReady
|
||||
log.Printf("Rabbit MQ ready %v\n", ready)
|
||||
|
||||
// subscribe to bound queue
|
||||
msgs, err := rabbitChan.Consume("orders", "", true, false, false, false, nil)
|
||||
failOnError(err, "Failed to consume")
|
||||
|
||||
for d := range msgs {
|
||||
log.Printf("Order %s\n", d.Body)
|
||||
log.Printf("Headers %v\n", d.Headers)
|
||||
id := getOrderId(d.Body)
|
||||
go createSpan(d.Headers, id)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Waiting for messages")
|
||||
forever := make(chan bool)
|
||||
<-forever
|
||||
}
|
@@ -10,3 +10,8 @@ services:
|
||||
- robot-shop
|
||||
depends_on:
|
||||
- web
|
||||
logging: &logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "25m"
|
||||
max-file: "2"
|
||||
|
@@ -6,14 +6,23 @@ services:
|
||||
image: ${REPO}/rs-mongodb:${TAG}
|
||||
networks:
|
||||
- robot-shop
|
||||
logging: &logging
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "25m"
|
||||
max-file: "2"
|
||||
redis:
|
||||
image: redis:4.0.6
|
||||
image: redis:6.2-alpine
|
||||
networks:
|
||||
- robot-shop
|
||||
logging:
|
||||
<<: *logging
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.7-management-alpine
|
||||
image: rabbitmq:3.8-management-alpine
|
||||
networks:
|
||||
- robot-shop
|
||||
logging:
|
||||
<<: *logging
|
||||
catalogue:
|
||||
build:
|
||||
context: catalogue
|
||||
@@ -22,6 +31,13 @@ services:
|
||||
- mongodb
|
||||
networks:
|
||||
- robot-shop
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost:8080/health" ]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
logging:
|
||||
<<: *logging
|
||||
user:
|
||||
build:
|
||||
context: user
|
||||
@@ -31,6 +47,13 @@ services:
|
||||
- redis
|
||||
networks:
|
||||
- robot-shop
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost:8080/health" ]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
logging:
|
||||
<<: *logging
|
||||
cart:
|
||||
build:
|
||||
context: cart
|
||||
@@ -39,6 +62,13 @@ services:
|
||||
- redis
|
||||
networks:
|
||||
- robot-shop
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost:8080/health" ]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
logging:
|
||||
<<: *logging
|
||||
mysql:
|
||||
build:
|
||||
context: mysql
|
||||
@@ -47,6 +77,8 @@ services:
|
||||
- NET_ADMIN
|
||||
networks:
|
||||
- robot-shop
|
||||
logging:
|
||||
<<: *logging
|
||||
shipping:
|
||||
build:
|
||||
context: shipping
|
||||
@@ -55,14 +87,30 @@ services:
|
||||
- mysql
|
||||
networks:
|
||||
- robot-shop
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
logging:
|
||||
<<: *logging
|
||||
ratings:
|
||||
build:
|
||||
context: ratings
|
||||
image: ${REPO}/rs-ratings:${TAG}
|
||||
environment:
|
||||
APP_ENV: prod
|
||||
networks:
|
||||
- robot-shop
|
||||
depends_on:
|
||||
- mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost/_health"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
logging:
|
||||
<<: *logging
|
||||
payment:
|
||||
build:
|
||||
context: payment
|
||||
@@ -71,9 +119,16 @@ services:
|
||||
- rabbitmq
|
||||
networks:
|
||||
- robot-shop
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost:8080/health"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
# Uncomment to change payment gateway
|
||||
#environment:
|
||||
#PAYMENT_GATEWAY: "https://www.worldpay.com"
|
||||
logging:
|
||||
<<: *logging
|
||||
dispatch:
|
||||
build:
|
||||
context: dispatch
|
||||
@@ -82,9 +137,14 @@ services:
|
||||
- rabbitmq
|
||||
networks:
|
||||
- robot-shop
|
||||
logging:
|
||||
<<: *logging
|
||||
web:
|
||||
build:
|
||||
context: web
|
||||
args:
|
||||
# agent key to download tracing libs
|
||||
KEY: ${INSTANA_AGENT_KEY}
|
||||
image: ${REPO}/rs-web:${TAG}
|
||||
depends_on:
|
||||
- catalogue
|
||||
@@ -95,11 +155,17 @@ services:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
- robot-shop
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "-H", "X-INSTANA-SYNTHETIC: 1", "-f", "http://localhost:8080/" ]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
# Uncomment to enable Instana EUM
|
||||
# environment:
|
||||
# INSTANA_EUM_KEY: <your eum key>
|
||||
# INSTANA_EUM_REPORTING_URL: https://eum-us-west-2.instana.io
|
||||
# INSTANA_EUM_REPORTING_URL: https://eum-eu-west-1.instana.io
|
||||
# INSTANA_EUM_REPORTING_URL: <your reporting url>
|
||||
logging:
|
||||
<<: *logging
|
||||
|
||||
networks:
|
||||
robot-shop:
|
||||
|
32
fluentd/Docker-Compose/README.md
Normal file
32
fluentd/Docker-Compose/README.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Configuration
|
||||
|
||||
Edit `fluent.conf` setting the parameters to match either your Humio account or Elasticsearch instance. See the [fluentd documentation](https://docs.fluentd.org/output/elasticsearch) and/or [Humio documentation](https://docs.humio.com/docs/ingesting-data/data-shippers/fluentd/) for details.
|
||||
|
||||
Start `fluentd` in a Docker container using the `run.sh` script.
|
||||
|
||||
## Docker Compose
|
||||
|
||||
To have all the containers in Stan's Robot Shop use fluentd for logging, the `docker-compose.yaml` needs to be edited. Change the logging section at the top of the file.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
mongodb:
|
||||
build:
|
||||
context: mongo
|
||||
image: ${REPO}/rs-mongodb:${TAG}
|
||||
networks:
|
||||
- robto-shop
|
||||
logging: &logging
|
||||
driver: "fluentd"
|
||||
options:
|
||||
fluentd-address: localhost:24224
|
||||
tag: "{{.ImageName}}"
|
||||
redis:
|
||||
```
|
||||
|
||||
If Robot Shop is already running, shut it down `docker-compose down`
|
||||
|
||||
Start Robot Shop with `docker-compose up -d`. It takes a few minutes to start, after that check with Humio or ELK for log entries.
|
||||
|
||||
Set up [logging integration](https://www.instana.com/docs/logging/) in Instana.
|
||||
|
24
fluentd/Docker-Compose/fluent.conf
Normal file
24
fluentd/Docker-Compose/fluent.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
<source>
|
||||
@type forward
|
||||
</source>
|
||||
|
||||
<filter **>
|
||||
@type record_transformer
|
||||
enable_ruby
|
||||
<record>
|
||||
docker.container_id ${record["container_id"]}
|
||||
docker.image_name ${tag}
|
||||
</record>
|
||||
</filter>
|
||||
|
||||
<match **>
|
||||
@type elasticsearch
|
||||
host cloud.humio.com
|
||||
port 9200
|
||||
scheme https
|
||||
ssl_version TLSv1_2
|
||||
user <Humio index or Elasticsearch user>
|
||||
password <Humio API key or Elasticsearch password>
|
||||
logstash_format true
|
||||
</match>
|
||||
|
12
fluentd/Docker-Compose/run.sh
Executable file
12
fluentd/Docker-Compose/run.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
IMAGE_NAME="robotshop/fluentd:elastic"
|
||||
|
||||
docker run \
|
||||
-d \
|
||||
--rm \
|
||||
--name fluentd \
|
||||
-p 24224:24224 \
|
||||
-v $(pwd)/fluent.conf:/fluentd/etc/fluent.conf \
|
||||
$IMAGE_NAME
|
||||
|
9
fluentd/Dockerfile
Normal file
9
fluentd/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM fluentd
|
||||
USER root
|
||||
RUN apk update && \
|
||||
apk add --virtual .build-dependencies build-base ruby-dev
|
||||
|
||||
RUN fluent-gem install fluent-plugin-elasticsearch && \
|
||||
fluent-gem install fluent-plugin-kubernetes_metadata_filter && \
|
||||
fluent-gem install fluent-plugin-multi-format-parser
|
||||
|
11
fluentd/Kubernetes/README.md
Normal file
11
fluentd/Kubernetes/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Kubernetes
|
||||
|
||||
Edit the `fluentd.yaml` file inserting your Humio or Elasticsearch instance details.
|
||||
|
||||
Apply the configuration:
|
||||
|
||||
```shell
|
||||
$ kubectl apply -f fluentd.yaml
|
||||
```
|
||||
|
||||
Set up [logging integration](https://www.instana.com/docs/logging/) in Instana.
|
148
fluentd/Kubernetes/fluentd.yaml
Normal file
148
fluentd/Kubernetes/fluentd.yaml
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: logging
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: fluentd
|
||||
namespace: logging
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: fluentd
|
||||
namespace: logging
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
- namespaces
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: fluentd
|
||||
namespace: logging
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: fluentd
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: fluentd
|
||||
namespace: logging
|
||||
#
|
||||
# CONFIGURATION
|
||||
#
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: fluentd-config
|
||||
namespace: logging
|
||||
data:
|
||||
fluent.conf: |
|
||||
<source>
|
||||
@type tail
|
||||
path /var/log/containers/*.log
|
||||
pos_file /var/log/fluentd-containers.log.pos
|
||||
tag kubernetes.*
|
||||
read_from_head false
|
||||
<parse>
|
||||
@type json
|
||||
</parse>
|
||||
</source>
|
||||
|
||||
<filter kubernetes.**>
|
||||
@type kubernetes_metadata
|
||||
@id filter_kube_metadata
|
||||
</filter>
|
||||
|
||||
# Throw away what is not needed first
|
||||
|
||||
#<match fluent.**>
|
||||
#@type null
|
||||
#</match>
|
||||
|
||||
<match kubernetes.var.log.containers.**kube-system**.log>
|
||||
@type null
|
||||
</match>
|
||||
|
||||
# Capture what is left
|
||||
<match **>
|
||||
@type elasticsearch
|
||||
host cloud.humio.com
|
||||
port 9200
|
||||
scheme https
|
||||
ssl_version TLSv1_2
|
||||
logstash_format true
|
||||
user <Humio index or Elasticsearch user>
|
||||
password <Humio API key or Elasticsearch password>
|
||||
</match>
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: fluentd
|
||||
namespace: logging
|
||||
labels:
|
||||
k8s-app: fluentd
|
||||
#https://github.com/kubernetes/kubernetes/issues/51376
|
||||
#kubernetes.io/cluster-service: "true"
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: fluentd
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: fluentd
|
||||
#kubernetes.io/cluster-service: "true"
|
||||
spec:
|
||||
serviceAccount: fluentd
|
||||
serviceAccountName: fluentd
|
||||
terminationGracePeriodSeconds: 30
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: fluentd
|
||||
image: robotshop/fluentd:elastic
|
||||
#args:
|
||||
# - "-v"
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 500Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 200Mi
|
||||
volumeMounts:
|
||||
- name: fluentd-config
|
||||
mountPath: /fluentd/etc
|
||||
- name: varlog
|
||||
mountPath: /var/log
|
||||
- name: varlibdockercontainers
|
||||
mountPath: /var/lib/docker/containers
|
||||
readOnly: true
|
||||
imagePullPolicy: Always
|
||||
volumes:
|
||||
- name: fluentd-config
|
||||
configMap:
|
||||
name: fluentd-config
|
||||
- name: varlog
|
||||
hostPath:
|
||||
path: /var/log
|
||||
- name: varlibdockercontainers
|
||||
hostPath:
|
||||
path: /var/lib/docker/containers
|
||||
|
10
fluentd/README.md
Normal file
10
fluentd/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Logging with Fluentd
|
||||
|
||||
This example works with [Humio](https://humio.com/) and [ELK](https://elastic.co/). Fluentd is used to ship the logs from the containers to the logging backend.
|
||||
|
||||
## Build Fluentd Container
|
||||
|
||||
The default `fluentd` Docker image does not include the output plugin for Elasticsearch. Therefore a new Docker image based on the default image with the Elasticsearch output plugin installed should be created, see the `Dockerfile` and `build.sh` script for examples. This example has already been built and pushed to Docker Hub.
|
||||
|
||||
Deployment is slightly different depending on which platform Robot Shop is run on. See the appropriate subdirectories for the required files and further instructions.
|
||||
|
12
fluentd/build.sh
Executable file
12
fluentd/build.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
|
||||
IMAGE_NAME="robotshop/fluentd:elastic"
|
||||
|
||||
|
||||
docker build -t "$IMAGE_NAME" .
|
||||
|
||||
if [ "$1" = "push" ]
|
||||
then
|
||||
docker push "$IMAGE_NAME"
|
||||
fi
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.6
|
||||
FROM python:3.9
|
||||
|
||||
# Some default values
|
||||
ENV HOST="http://localhost:8080/" \
|
||||
@@ -16,6 +16,7 @@ RUN pip install -r requirements.txt
|
||||
|
||||
COPY entrypoint.sh /load/
|
||||
COPY robot-shop.py /load/
|
||||
COPY utilities /load/
|
||||
|
||||
CMD ["./entrypoint.sh"]
|
||||
|
||||
|
@@ -14,7 +14,20 @@ $ ./load-gen.sh
|
||||
|
||||
Runs the load generation script against the application started with `docker-compose up` . There are various command line options to configure the load.
|
||||
|
||||
Alternatively, you can run the Container from Docker Hub directly on one of the nodes having access to the web service:
|
||||
The script must be run in the load-gen directory. It logs all the php API calls into the file logs/php_services_calls.csv.
|
||||
|
||||
This command launches the load generator to run undefinitely (i.e. without time limits), simulating 5 clients calling the API reachable at the default URL http://localhost:8080:
|
||||
|
||||
```shell
|
||||
$ ./load-gen.sh \
|
||||
-h http://host:port/
|
||||
-n 5 \
|
||||
-v
|
||||
```
|
||||
|
||||
The command also logs comprehensive details of all the PHP API called in the file logs/calls.log, triggered by the option `-v` .
|
||||
|
||||
Alternatively, you can run the Container from Docker Hub directly on one of the nodes having access to the web service. Here there is an example of how to do it and an explanation for the variables involved:
|
||||
|
||||
```shell
|
||||
$ docker run \
|
||||
@@ -22,11 +35,14 @@ $ docker run \
|
||||
--rm \
|
||||
--name="loadgen" \
|
||||
--network=host \
|
||||
--volume ${PWD}/logs:/load/logs \
|
||||
--volume ${PWD}/utilities:/load/utilities \
|
||||
-e "HOST=http://host:8080/"
|
||||
-e "NUM_CLIENTS=5" \
|
||||
-e "RUN_TIME=1h30m" \
|
||||
-e "ERROR=1" \
|
||||
-e "SILENT=1" \
|
||||
-e "LOAD_DEBUG=0" \
|
||||
robotshop/rs-load
|
||||
```
|
||||
|
||||
@@ -36,7 +52,11 @@ Set the following environment variables to configure the load:
|
||||
* NUM_CLIENTS - How many simultaneous load scripts to run, the bigger the number the bigger the load. The default is 1
|
||||
* RUN_TIME - For NUM_CLIENTS greater than 1 the duration to run. If not set, load is run for ever with NUM_CLIENTS. See below.
|
||||
* ERROR - Set this to 1 to have erroroneous calls made to the payment service.
|
||||
* SILENT - Set this to 1 to surpress the very verbose output from the script. This is a good idea if you're going to run load for more than a few minutes.
|
||||
* SILENT - Set this to 1 to surpress the very verbose output to the stdout from the script. This is a good idea if you're going to run load for more than a few minutes.
|
||||
* LOAD_DEBUG - Set this to 1 to enable the output of every API call produced from the script into the log file logs/calls.log. This is a good idea if you're going to investigate over occurred events during load generation.
|
||||
|
||||
The load generator logs all the PHP API calls into the file logs/php_services_calls.csv, despite the variables SILENT and LOAD_DEBUG being set, respectively, to 1 and 0.
|
||||
The content of the directory logs is cleaned everytime the script load-gen.sh is called.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
@@ -67,4 +87,4 @@ $ ./load-gen.sh \
|
||||
-t 1h30m
|
||||
```
|
||||
|
||||
The load will be run with `10` clients for `1h30m` before dropping down to `1` client for `1h30m` then looping back to `10` clients etc.
|
||||
The load will be run with `10` clients for `1h30m` before dropping down to `1` client for `1h30m` then looping back to `10` clients etc.
|
||||
|
@@ -35,11 +35,13 @@ else
|
||||
unset TIME
|
||||
fi
|
||||
|
||||
mkdir -p logs;
|
||||
|
||||
echo "Starting $CLIENTS clients for ${RUN_TIME:-ever}"
|
||||
if [ "$SILENT" -eq 1 ]
|
||||
then
|
||||
locust -f robot-shop.py --host "$HOST" --no-web -r 1 -c $NUM_CLIENTS $TIME > /dev/null 2>&1
|
||||
locust -f robot-shop.py --host "$HOST" --headless -r 1 -u $NUM_CLIENTS $TIME > /dev/null 2>&1
|
||||
else
|
||||
locust -f robot-shop.py --host "$HOST" --no-web -r 1 -c $NUM_CLIENTS $TIME
|
||||
locust -f robot-shop.py --host "$HOST" --headless -r 1 -u $NUM_CLIENTS $TIME
|
||||
fi
|
||||
|
||||
|
@@ -15,6 +15,9 @@ HOST="http://localhost:8080"
|
||||
# Error flag
|
||||
ERROR=0
|
||||
|
||||
# Verbose mode
|
||||
LOAD_DEBUG=1
|
||||
|
||||
# Daemon flag
|
||||
DAEMON="-it"
|
||||
SILENT=0
|
||||
@@ -24,7 +27,8 @@ USAGE="\
|
||||
loadgen.sh
|
||||
|
||||
e - error flag
|
||||
d - run in background
|
||||
v - verbose mode. It implies the setting of LOAD_DEBUG and of error flag. The load debug will still be printed in the stdout, even if the stdout is suppressed.
|
||||
d - run in background. It implies the setting of SILENT mode, so the stdout will not be floaded. If -v is used, the load debug will still be printed in the stdout, even if the stdout is suppressed.
|
||||
n - number of clients
|
||||
t - time to run n clients
|
||||
h - target host
|
||||
@@ -42,12 +46,16 @@ eval $(egrep '[A-Z]+=' ../.env)
|
||||
echo "Repo $REPO"
|
||||
echo "Tag $TAG"
|
||||
|
||||
while getopts 'edn:t:h:' OPT
|
||||
while getopts 'edvn:t:h:' OPT
|
||||
do
|
||||
case $OPT in
|
||||
e)
|
||||
ERROR=1
|
||||
;;
|
||||
v)
|
||||
LOAD_DEBUG=1
|
||||
ERROR=1
|
||||
;;
|
||||
d)
|
||||
DAEMON="-d"
|
||||
SILENT=1
|
||||
@@ -92,9 +100,13 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
rm -rf logs/*
|
||||
|
||||
docker run \
|
||||
$DAEMON \
|
||||
--name loadgen \
|
||||
--volume ${PWD}/logs:/load/logs \
|
||||
--volume ${PWD}/utilities:/load/utilities \
|
||||
--rm \
|
||||
--network=host \
|
||||
-e "HOST=$HOST" \
|
||||
@@ -102,5 +114,6 @@ docker run \
|
||||
-e "RUN_TIME=$RUN_TIME" \
|
||||
-e "SILENT=$SILENT" \
|
||||
-e "ERROR=$ERROR" \
|
||||
-e "LOAD_DEBUG=$LOAD_DEBUG" \
|
||||
${REPO}/rs-load:${TAG}
|
||||
|
||||
|
0
load-gen/logs/.gitkeep
Normal file
0
load-gen/logs/.gitkeep
Normal file
@@ -1 +1 @@
|
||||
locustio
|
||||
locust
|
||||
|
@@ -1,33 +1,96 @@
|
||||
import os
|
||||
from locust import HttpLocust, TaskSet, task
|
||||
import random
|
||||
import logging
|
||||
|
||||
from locust import HttpUser, task, between
|
||||
from utilities.CSVWriter import CSVWriter
|
||||
from random import choice
|
||||
from random import randint
|
||||
from sys import argv
|
||||
from datetime import date
|
||||
|
||||
class UserBehavior(HttpUser):
|
||||
wait_time = between(2, 10)
|
||||
|
||||
# source: https://tools.tracemyip.org/search--ip/list
|
||||
fake_ip_addresses = [
|
||||
# white house
|
||||
"156.33.241.5",
|
||||
# Hollywood
|
||||
"34.196.93.245",
|
||||
# Chicago
|
||||
"98.142.103.241",
|
||||
# Los Angeles
|
||||
"192.241.230.151",
|
||||
# Berlin
|
||||
"46.114.35.116",
|
||||
# Singapore
|
||||
"52.77.99.130",
|
||||
# Sydney
|
||||
"60.242.161.215"
|
||||
]
|
||||
|
||||
logger = None
|
||||
rthandler = None
|
||||
formatter = None
|
||||
php_services_api_prefix = '/api/ratings/api'
|
||||
php_service_rate = '/rate'
|
||||
php_service_fetch = '/fetch'
|
||||
php_fieldnames = ['REQTYPE', 'SERVICE', 'INPUT', 'HEADER', 'ERRFLAG']
|
||||
my_csv_writer = None
|
||||
|
||||
class UserBehavior(TaskSet):
|
||||
def on_start(self):
|
||||
""" on_start is called when a Locust start before any task is scheduled """
|
||||
print('Starting')
|
||||
print("ARGS ARE:\n\"")
|
||||
print("\n".join(argv))
|
||||
print('End of ARGS \n')
|
||||
|
||||
for handler in logging.root.handlers[:]:
|
||||
logging.root.removeHandler(handler)
|
||||
|
||||
self.logger = logging.getLogger('simple_rotating_logger')
|
||||
self.rthandler = logging.handlers.RotatingFileHandler(filename='logs/calls.log', mode='a', maxBytes=5242880, backupCount=20, encoding='utf-8')
|
||||
self.formatter = logging.Formatter('%(asctime)s [%(levelname)s]:%(message)s')
|
||||
self.rthandler.setFormatter(self.formatter)
|
||||
self.logger.addHandler(self.rthandler)
|
||||
|
||||
if os.environ.get('LOAD_DEBUG') == '1':
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
self.logger.setLevel(logging.WARNING)
|
||||
|
||||
self.logger.info('Starting')
|
||||
self.logger.info('LOAD_DEBUG: %s', os.environ.get("LOAD_DEBUG"))
|
||||
self.logger.info('on start. php_fieldnames: %s', format(self.php_fieldnames))
|
||||
|
||||
self.my_csv_writer = CSVWriter("logs/php_services_calls.csv", self.php_fieldnames)
|
||||
|
||||
@task
|
||||
def login(self):
|
||||
fake_ip = random.choice(self.fake_ip_addresses)
|
||||
|
||||
credentials = {
|
||||
'name': 'user',
|
||||
'password': 'password'
|
||||
}
|
||||
res = self.client.post('/api/user/login', json=credentials)
|
||||
res = self.client.post('/api/user/login', json=credentials, headers={'x-forwarded-for': fake_ip})
|
||||
print('login {}'.format(res.status_code))
|
||||
|
||||
|
||||
@task
|
||||
def load(self):
|
||||
self.client.get('/')
|
||||
user = self.client.get('/api/user/uniqueid').json()
|
||||
self.logger.info('new user, new load task\n')
|
||||
fake_ip = random.choice(self.fake_ip_addresses)
|
||||
|
||||
self.client.get('/', headers={'x-forwarded-for': fake_ip})
|
||||
user = self.client.get('/api/user/uniqueid', headers={'x-forwarded-for': fake_ip}).json()
|
||||
uniqueid = user['uuid']
|
||||
print('User {}'.format(uniqueid))
|
||||
|
||||
self.client.get('/api/catalogue/categories')
|
||||
self.client.get('/api/catalogue/categories', headers={'x-forwarded-for': fake_ip})
|
||||
# all products in catalogue
|
||||
products = self.client.get('/api/catalogue/products').json()
|
||||
products = self.client.get('/api/catalogue/products', headers={'x-forwarded-for': fake_ip}).json()
|
||||
for i in range(2):
|
||||
item = None
|
||||
while True:
|
||||
@@ -35,41 +98,60 @@ class UserBehavior(TaskSet):
|
||||
if item['instock'] != 0:
|
||||
break
|
||||
|
||||
headers={'x-forwarded-for': fake_ip}
|
||||
# vote for item
|
||||
if randint(1, 10) <= 3:
|
||||
self.client.put('/api/ratings/api/rate/{}/{}'.format(item['sku'], randint(1, 5)))
|
||||
ratevalue = randint(1, 5)
|
||||
put_rate_api_str = '{}{}/{}/{}'.format(self.php_services_api_prefix, self.php_service_rate, item['sku'], ratevalue )
|
||||
self.logger.info('item: {} ratevalue: {} put_rate_api_str: {} by: {}\n'.format(item['sku'], ratevalue, put_rate_api_str, fake_ip))
|
||||
try:
|
||||
self.client.put(put_rate_api_str, headers)
|
||||
self.my_csv_writer.writerow({'REQTYPE': 'PUT', 'SERVICE': '{}'.format(self.php_service_rate), 'INPUT': '{}/{}'.format(item['sku'], ratevalue ), 'HEADER': '{}'.format(headers), 'ERRFLAG': '{}'.format("")})
|
||||
except BaseException as err:
|
||||
self.logger.warnign("Last call generated an error")
|
||||
self.logger.exception()
|
||||
self.my_csv_writer.writerow({'REQTYPE': 'PUT', 'SERVICE': '{}'.format(self.php_service_rate), 'INPUT': '{}/{}'.format(item['sku'], ratevalue ), 'HEADER': '{}'.format(headers), 'ERRFLAG': '{}'.format(err)})
|
||||
pass
|
||||
|
||||
self.client.get('/api/catalogue/product/{}'.format(item['sku']))
|
||||
self.client.get('/api/ratings/api/fetch/{}'.format(item['sku']))
|
||||
self.client.get('/api/cart/add/{}/{}/1'.format(uniqueid, item['sku']))
|
||||
self.client.get('/api/catalogue/product/{}'.format(item['sku']), headers={'x-forwarded-for': fake_ip})
|
||||
|
||||
cart = self.client.get('/api/cart/cart/{}'.format(uniqueid)).json()
|
||||
get_rate_api_str = '{}{}/{}'.format(self.php_services_api_prefix, self.php_service_fetch, item['sku'])
|
||||
self.logger.info('item: {} get_rate_api_str: {} by: {}\n'.format(item['sku'], get_rate_api_str, fake_ip))
|
||||
try:
|
||||
self.client.get(get_rate_api_str, headers={'x-forwarded-for': fake_ip})
|
||||
self.my_csv_writer.writerow({'REQTYPE': 'GET', 'SERVICE': '{}'.format(self.php_service_fetch), 'INPUT': '{}'.format(item['sku']), 'HEADER': '{}'.format(headers), 'ERRFLAG': '{}'.format("") })
|
||||
except BaseException as err:
|
||||
self.logger.warnign("Last call generated an error")
|
||||
self.logger.exception()
|
||||
self.my_csv_writer.writerow({'REQTYPE': 'GET', 'SERVICE': '{}'.format(self.php_service_fetch), 'INPUT': '{}'.format(item['sku']), 'HEADER': '{}'.format(headers), 'ERRFLAG': '{}'.format(err) })
|
||||
pass
|
||||
|
||||
self.client.get('/api/cart/add/{}/{}/1'.format(uniqueid, item['sku']), headers={'x-forwarded-for': fake_ip})
|
||||
|
||||
cart = self.client.get('/api/cart/cart/{}'.format(uniqueid), headers={'x-forwarded-for': fake_ip}).json()
|
||||
item = choice(cart['items'])
|
||||
self.client.get('/api/cart/update/{}/{}/2'.format(uniqueid, item['sku']))
|
||||
self.client.get('/api/cart/update/{}/{}/2'.format(uniqueid, item['sku']), headers={'x-forwarded-for': fake_ip})
|
||||
|
||||
# country codes
|
||||
code = choice(self.client.get('/api/shipping/codes').json())
|
||||
city = choice(self.client.get('/api/shipping/cities/{}'.format(code['code'])).json())
|
||||
code = choice(self.client.get('/api/shipping/codes', headers={'x-forwarded-for': fake_ip}).json())
|
||||
city = choice(self.client.get('/api/shipping/cities/{}'.format(code['code']), headers={'x-forwarded-for': fake_ip}).json())
|
||||
print('code {} city {}'.format(code, city))
|
||||
shipping = self.client.get('/api/shipping/calc/{}'.format(city['uuid'])).json()
|
||||
shipping = self.client.get('/api/shipping/calc/{}'.format(city['uuid']), headers={'x-forwarded-for': fake_ip}).json()
|
||||
shipping['location'] = '{} {}'.format(code['name'], city['name'])
|
||||
print('Shipping {}'.format(shipping))
|
||||
# POST
|
||||
cart = self.client.post('/api/shipping/confirm/{}'.format(uniqueid), json=shipping).json()
|
||||
cart = self.client.post('/api/shipping/confirm/{}'.format(uniqueid), json=shipping, headers={'x-forwarded-for': fake_ip}).json()
|
||||
print('Final cart {}'.format(cart))
|
||||
|
||||
order = self.client.post('/api/payment/pay/{}'.format(uniqueid), json=cart).json()
|
||||
order = self.client.post('/api/payment/pay/{}'.format(uniqueid), json=cart, headers={'x-forwarded-for': fake_ip}).json()
|
||||
print('Order {}'.format(order))
|
||||
|
||||
@task
|
||||
def error(self):
|
||||
fake_ip = random.choice(self.fake_ip_addresses)
|
||||
if os.environ.get('ERROR') == '1':
|
||||
print('Error request')
|
||||
cart = {'total': 0, 'tax': 0}
|
||||
self.client.post('/api/payment/pay/partner-57', json=cart)
|
||||
self.client.post('/api/payment/pay/partner-57', json=cart, headers={'x-forwarded-for': fake_ip})
|
||||
|
||||
|
||||
class WebsiteUser(HttpLocust):
|
||||
task_set = UserBehavior
|
||||
min_wait = 1000
|
||||
max_wait = 5000
|
||||
|
19
load-gen/utilities/CSVReader.py
Normal file
19
load-gen/utilities/CSVReader.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import csv
|
||||
|
||||
# Refs:
|
||||
# [Tutorial1] https://rharshad.com/locust-load-test/
|
||||
class CSVReader:
|
||||
def __init__(self, filepath):
|
||||
try:
|
||||
file = open(filepath)
|
||||
except TypeError:
|
||||
pass
|
||||
self.file = file
|
||||
self.reader = csv.reader(file)
|
||||
|
||||
def __next__(self):
|
||||
try:
|
||||
return next(self.reader)
|
||||
except StopIteration:
|
||||
self.file.seek(0, 0)
|
||||
return next(self.reader)
|
45
load-gen/utilities/CSVWriter.py
Normal file
45
load-gen/utilities/CSVWriter.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
import csv
|
||||
from datetime import date
|
||||
|
||||
class CSVWriter:
|
||||
def __init__(self, filepath, custom_fields=[]):
|
||||
if os.environ.get('LOAD_DEBUG') == '1':
|
||||
print('CSVWriter init/args: filepath:\'{}\' custom_fields\'{}\''.format(filepath,custom_fields))
|
||||
|
||||
try:
|
||||
file = open(filepath,'a')
|
||||
except TypeError as typeErr:
|
||||
print(f"CSVWriter init/Unexpected {err=}, {type(typeErr)=}")
|
||||
raise
|
||||
|
||||
print('CSVWriter init/pass')
|
||||
self.file = file
|
||||
|
||||
if not custom_fields:
|
||||
print('CSVWriter init/empty params')
|
||||
else:
|
||||
self.fieldnames = custom_fields
|
||||
|
||||
try:
|
||||
self.writer = csv.DictWriter(self.file, self.fieldnames)
|
||||
self.writer.writeheader()
|
||||
except BaseException as err:
|
||||
print(f"CSVWriter init/Unexpected {err=}, {type(err)=}")
|
||||
raise
|
||||
|
||||
self.file.flush()
|
||||
|
||||
def writerow(self, row: dict):
|
||||
if os.environ.get('LOAD_DEBUG') == '1':
|
||||
print('CSVWriter/writerow/args: {}',format(row))
|
||||
|
||||
if not row:
|
||||
print('CSVWriter/writerow/empty arg')
|
||||
else:
|
||||
try:
|
||||
self.writer.writerow(row)
|
||||
self.file.flush()
|
||||
except BaseException as err:
|
||||
print(f"CSVWriter/writerow/Unexpected {err=}, {type(err)=}")
|
||||
raise
|
@@ -1,4 +1,4 @@
|
||||
FROM mongo:3.6.1
|
||||
FROM mongo:5
|
||||
|
||||
COPY *.js /docker-entrypoint-initdb.d/
|
||||
|
||||
|
@@ -3,17 +3,17 @@
|
||||
//
|
||||
db = db.getSiblingDB('catalogue');
|
||||
db.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: 953, instock: 1, categories: ['Robot']},
|
||||
{sku: 'R2D2', name: 'R2D2', description: 'R2 maintenance robot and secret messenger. Help me Obi Wan', price: 1024, 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: 67, 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']}
|
||||
{sku: 'Watson', name: 'Watson', description: 'Probably the smartest AI on the planet', price: 2001, instock: 2, categories: ['Artificial Intelligence']},
|
||||
{sku: 'Ewooid', name: 'Ewooid', description: 'Fully sentient assistant', price: 200, instock: 0, categories: ['Artificial Intelligence']},
|
||||
{sku: 'HPTD', name: 'High-Powered Travel Droid', description: 'Traveling to the far reaches of the Galaxy? You need this for protection. Comes in handy when you are lost in space', price: 1200, instock: 12, categories: ['Robot']},
|
||||
{sku: 'UHJ', name: 'Ultimate Harvesting Juggernaut', description: 'Extraterrestrial vegetation harvester', price: 5000, instock: 10, categories: ['Robot']},
|
||||
{sku: 'EPE', name: 'Extreme Probe Emulator', description: 'Versatile interface adapter for hacking into systems', price: 953, instock: 1, categories: ['Robot']},
|
||||
{sku: 'EMM', name: 'Exceptional Medical Machine', description: 'Fully automatic surgery droid with exceptional bedside manner', price: 1024, instock: 1, categories: ['Robot']},
|
||||
{sku: 'SHCE', name: 'Strategic Human Control Emulator', description: 'Diplomatic protocol assistant', price: 300, instock: 12, categories: ['Robot']},
|
||||
{sku: 'RED', name: 'Responsive Enforcer Droid', description: 'Security detail, will gaurd anything', price: 700, instock: 5, categories: ['Robot']},
|
||||
{sku: 'RMC', name: 'Robotic Mining Cyborg', description: 'Excellent tunneling capability to get those rare minerals', price: 42, instock: 48, categories: ['Robot']},
|
||||
{sku: 'STAN-1', name: 'Stan', description: 'Observability guru', price: 67, instock: 1000, categories: ['Robot', 'Artificial Intelligence']},
|
||||
{sku: 'CNA', name: 'Cybernated Neutralization Android', description: 'Is your spaceship a bit whiffy? This little fellow will bring a breath of fresh air', price: 1000, instock: 0, categories: ['Robot']}
|
||||
]);
|
||||
|
||||
// full text index for searching
|
||||
|
@@ -1,4 +1,6 @@
|
||||
FROM mysql:5.7.20
|
||||
FROM mysql:5.7
|
||||
|
||||
VOLUME /data
|
||||
|
||||
ENV MYSQL_ALLOW_EMPTY_PASSWORD=yes \
|
||||
MYSQL_DATABASE=cities \
|
||||
@@ -11,6 +13,6 @@ RUN /root/config.sh
|
||||
|
||||
COPY scripts/* /docker-entrypoint-initdb.d/
|
||||
|
||||
RUN /entrypoint.sh mysqld & while [ ! -f /tmp/finished ]; do sleep 10; done
|
||||
RUN rm /docker-entrypoint-initdb.d/*
|
||||
#RUN /entrypoint.sh mysqld & while [ ! -f /tmp/finished ]; do sleep 10; done
|
||||
#RUN rm /docker-entrypoint-initdb.d/*
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM python:3.6
|
||||
FROM python:3.9
|
||||
|
||||
EXPOSE 8080
|
||||
USER root
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import random
|
||||
|
||||
import instana
|
||||
import os
|
||||
import sys
|
||||
@@ -7,8 +9,6 @@ import uuid
|
||||
import json
|
||||
import requests
|
||||
import traceback
|
||||
import opentracing as ot
|
||||
import opentracing.ext.tags as tags
|
||||
from flask import Flask
|
||||
from flask import Response
|
||||
from flask import request
|
||||
@@ -59,11 +59,6 @@ def pay(id):
|
||||
|
||||
anonymous_user = True
|
||||
|
||||
# add some log info to the active trace
|
||||
span = ot.tracer.active_span
|
||||
span.log_kv({'id': id})
|
||||
span.log_kv({'cart': cart})
|
||||
|
||||
# check user exists
|
||||
try:
|
||||
req = requests.get('http://{user}:8080/check/{id}'.format(user=USER, id=id))
|
||||
@@ -131,36 +126,13 @@ def pay(id):
|
||||
|
||||
def queueOrder(order):
|
||||
app.logger.info('queue order')
|
||||
# RabbitMQ pika is not currently traced automatically
|
||||
# opentracing tracer is automatically set to Instana tracer
|
||||
# start a span
|
||||
|
||||
parent_span = ot.tracer.active_span
|
||||
with ot.tracer.start_active_span('queueOrder', child_of=parent_span,
|
||||
tags={
|
||||
'exchange': Publisher.EXCHANGE,
|
||||
'key': Publisher.ROUTING_KEY
|
||||
}) as tscope:
|
||||
tscope.span.set_tag('span.kind', 'intermediate')
|
||||
tscope.span.log_kv({'orderid': order.get('orderid')})
|
||||
with ot.tracer.start_active_span('rabbitmq', child_of=tscope.span,
|
||||
tags={
|
||||
'exchange': Publisher.EXCHANGE,
|
||||
'sort': 'publish',
|
||||
'address': Publisher.HOST,
|
||||
'key': Publisher.ROUTING_KEY
|
||||
}
|
||||
) as scope:
|
||||
# For screenshot demo requirements optionally add in a bit of delay
|
||||
delay = int(os.getenv('PAYMENT_DELAY_MS', 0))
|
||||
time.sleep(delay / 1000)
|
||||
|
||||
# For screenshot demo requirements optionally add in a bit of delay
|
||||
delay = int(os.getenv('PAYMENT_DELAY_MS', 0))
|
||||
time.sleep(delay / 1000)
|
||||
|
||||
headers = {}
|
||||
ot.tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, headers)
|
||||
app.logger.info('msg headers {}'.format(headers))
|
||||
|
||||
publisher.publish(order, headers)
|
||||
headers = {}
|
||||
publisher.publish(order, headers)
|
||||
|
||||
|
||||
def countItems(items):
|
||||
|
11
pullbaseimages.sh
Executable file
11
pullbaseimages.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
for DFILE in $(find . -name Dockerfile -print)
|
||||
do
|
||||
# multiple images
|
||||
for IMAGE in $(awk '/^FROM/ { print $2 }' $DFILE)
|
||||
do
|
||||
echo "Pulling $IMAGE"
|
||||
docker pull $IMAGE
|
||||
done
|
||||
done
|
@@ -1,16 +1,13 @@
|
||||
# Use composer to install dependencies
|
||||
FROM composer AS build
|
||||
|
||||
COPY composer.json /app/
|
||||
|
||||
RUN composer install
|
||||
|
||||
#
|
||||
# Build the app
|
||||
#
|
||||
FROM php:7.3-apache
|
||||
FROM php:7.4-apache
|
||||
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN apt-get update && apt-get install -yqq unzip libzip-dev \
|
||||
&& docker-php-ext-install pdo_mysql opcache zip
|
||||
|
||||
# Enable AutoProfile for PHP which is currently opt-in beta
|
||||
RUN echo "instana.enable_auto_profile=1" > "/usr/local/etc/php/conf.d/zzz-instana-extras.ini"
|
||||
|
||||
# relax permissions on status
|
||||
COPY status.conf /etc/apache2/mods-available/status.conf
|
||||
@@ -19,8 +16,16 @@ RUN a2enmod rewrite && a2enmod status
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# copy dependencies from previous step
|
||||
COPY --from=build /app/vendor/ /var/www/html/vendor/
|
||||
|
||||
COPY html/ /var/www/html
|
||||
|
||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
||||
RUN composer install
|
||||
|
||||
# This is important. Symfony needs write permissions and we
|
||||
# dont know the context in which the container will run, i.e.
|
||||
# which user will be forced from the outside so better play
|
||||
# safe for this simple demo.
|
||||
RUN rm -Rf /var/www/var/*
|
||||
RUN chown -R www-data /var/www
|
||||
RUN chmod -R 777 /var/www
|
||||
|
||||
|
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"require": {
|
||||
"monolog/monolog": "^1.24.0"
|
||||
}
|
||||
}
|
@@ -1,6 +1,10 @@
|
||||
DirectoryIndex index.php
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteCond %{ENV:REDIRECT_STATUS} =""
|
||||
RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
|
||||
RewriteCond %{REQUEST_URI} !=/server-status
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule api/(.*)$ api.php?request=$1 [QSA,NC,L]
|
||||
RewriteRule ^ %{ENV:BASE}/index.php [L]
|
||||
</IfModule>
|
||||
|
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
// load composer installed files
|
||||
require_once(__DIR__.'/vendor/autoload.php');
|
||||
use Monolog\Logger;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
abstract class API {
|
||||
protected $method = '';
|
||||
|
||||
protected $endpoint = '';
|
||||
|
||||
protected $verb = '';
|
||||
|
||||
protected $args = array();
|
||||
|
||||
protected $file = Null;
|
||||
|
||||
protected $logger = Null;
|
||||
|
||||
protected $logHandler = Null;
|
||||
|
||||
public function __construct($request) {
|
||||
// Logging
|
||||
$this->logHandler = new StreamHandler('php://stdout', Logger::INFO);
|
||||
|
||||
// CORS
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: *');
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$this->args = explode('/', rtrim($request, '/'));
|
||||
$this->endpoint = array_shift($this->args);
|
||||
|
||||
if(array_key_exists(0, $this->args) && !is_numeric($this->args[0])) {
|
||||
$this->verb = array_shift($this->args);
|
||||
}
|
||||
|
||||
$this->method = $_SERVER['REQUEST_METHOD'];
|
||||
if($this->method == 'POST' && array_key_exists('HTTP_X_METHOD', $_SERVER)) {
|
||||
if($_SERVER['HTTP_X_HTTP_METHOD'] == 'DELETE') {
|
||||
$this->method = 'DELETE';
|
||||
} else if($_SERVER['HTTP_X_HTTP_METHOD'] == 'PUT') {
|
||||
$this->method = 'PUT';
|
||||
} else {
|
||||
throw new Exception('Unexpected header');
|
||||
}
|
||||
}
|
||||
|
||||
switch($this->method) {
|
||||
case 'DELETE':
|
||||
case 'POST':
|
||||
$this->request = $this->_cleanInputs($_POST);
|
||||
break;
|
||||
case 'GET':
|
||||
$this->request = $this->_cleanInputs($_GET);
|
||||
break;
|
||||
case 'PUT':
|
||||
$this->request = $this->_cleanInputs($_GET);
|
||||
$this->file = file_get_contents('php://input');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function processAPI() {
|
||||
if(method_exists($this, $this->endpoint)) {
|
||||
try {
|
||||
$result = $this->{$this->endpoint}();
|
||||
return $this->_response($result, 200);
|
||||
} catch (Exception $e) {
|
||||
return $this->_response($e->getMessage(), $e->getCode());
|
||||
}
|
||||
}
|
||||
return $this->_response("No endpoint: $this->endpoint", 404);
|
||||
}
|
||||
|
||||
private function _response($data, $status = 200) {
|
||||
header('HTTP/1.1 ' . $status . ' ' . $this->_requestStatus($status));
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
private function _cleanInputs($data) {
|
||||
$clean_input = array();
|
||||
|
||||
if(is_array($data)) {
|
||||
foreach($data as $k => $v) {
|
||||
$clean_input[$k] = $this->_cleanInputs($v);
|
||||
}
|
||||
} else {
|
||||
$clean_input = trim(strip_tags($data));
|
||||
}
|
||||
|
||||
return $clean_input;
|
||||
}
|
||||
|
||||
private function _requestStatus($code) {
|
||||
$status = array(
|
||||
200 => 'OK',
|
||||
400 => 'Bad Request',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
500 => 'Internal Server Error');
|
||||
|
||||
return (array_key_exists("$code", $status) ? $status["$code"] : $status['500']);
|
||||
}
|
||||
}
|
||||
?>
|
@@ -1,179 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
// 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', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// ratings/rate/sku/score
|
||||
protected function rate() {
|
||||
if($this->method == 'PUT' && 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', 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';
|
||||
$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, 'ratings', 'iloveit', $opt);
|
||||
} catch (PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
$this->logger->error("Database error $msg");
|
||||
$db = false;
|
||||
}
|
||||
|
||||
return $db;
|
||||
}
|
||||
|
||||
// check sku exists in product catalogue
|
||||
private function _checkSku($sku) {
|
||||
$url = getenv('CATALOGUE_URL') ? getenv('CATALOGUE_URL') : 'http://catalogue:8080/';
|
||||
$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', 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()));
|
||||
}
|
||||
?>
|
24
ratings/html/composer.json
Normal file
24
ratings/html/composer.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"require": {
|
||||
"php": "^7.4",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"psr/log": "*",
|
||||
"monolog/monolog": "^1.24.0",
|
||||
"symfony/config": "^5.2",
|
||||
"symfony/http-kernel": "^5.2",
|
||||
"symfony/http-foundation": "^5.2",
|
||||
"symfony/routing": "^5.2",
|
||||
"symfony/dependency-injection": "^5.2",
|
||||
"symfony/framework-bundle": "^5.2",
|
||||
"doctrine/annotations": "^1.10",
|
||||
"symfony/monolog-bundle": "^3.5",
|
||||
"instana/instana-php-sdk": "^1.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Instana\\RobotShop\\Ratings\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
15
ratings/html/index.php
Normal file
15
ratings/html/index.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
use Instana\RobotShop\Ratings\Kernel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
$env = getenv('APP_ENV') ?: 'dev';
|
||||
$kernel = new Kernel($env, true);
|
||||
$request = Request::createFromGlobals();
|
||||
$response = $kernel->handle($request);
|
||||
$response->send();
|
||||
$kernel->terminate($request, $response);
|
@@ -1 +1,3 @@
|
||||
<?php phpinfo(); ?>
|
||||
<?php
|
||||
|
||||
phpinfo();
|
||||
|
46
ratings/html/src/Controller/HealthController.php
Normal file
46
ratings/html/src/Controller/HealthController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Controller;
|
||||
|
||||
use Instana\RobotShop\Ratings\Service\HealthCheckService;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/_health")
|
||||
*/
|
||||
class HealthController implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var HealthCheckService
|
||||
*/
|
||||
private $healthCheckService;
|
||||
|
||||
public function __construct(HealthCheckService $healthCheckService)
|
||||
{
|
||||
$this->healthCheckService = $healthCheckService;
|
||||
}
|
||||
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
$checks = [];
|
||||
try {
|
||||
$this->healthCheckService->checkConnectivity();
|
||||
$checks['pdo_connectivity'] = true;
|
||||
} catch (\PDOException $e) {
|
||||
$checks['pdo_connectivity'] = false;
|
||||
}
|
||||
|
||||
$this->logger->info('Health-Check', $checks);
|
||||
|
||||
return new JsonResponse($checks, $checks['pdo_connectivity'] ? Response::HTTP_OK : Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
90
ratings/html/src/Controller/RatingsApiController.php
Normal file
90
ratings/html/src/Controller/RatingsApiController.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Controller;
|
||||
|
||||
use Instana\RobotShop\Ratings\Service\CatalogueService;
|
||||
use Instana\RobotShop\Ratings\Service\RatingsService;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route("/api")
|
||||
*/
|
||||
class RatingsApiController implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var RatingsService
|
||||
*/
|
||||
private $ratingsService;
|
||||
|
||||
/**
|
||||
* @var CatalogueService
|
||||
*/
|
||||
private $catalogueService;
|
||||
|
||||
public function __construct(CatalogueService $catalogueService, RatingsService $ratingsService)
|
||||
{
|
||||
$this->ratingsService = $ratingsService;
|
||||
$this->catalogueService = $catalogueService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/rate/{sku}/{score}", methods={"PUT"})
|
||||
*/
|
||||
public function put(Request $request, string $sku, int $score): Response
|
||||
{
|
||||
$score = min(max(1, $score), 5);
|
||||
|
||||
try {
|
||||
if (false === $this->catalogueService->checkSKU($sku)) {
|
||||
throw new NotFoundHttpException("$sku not found");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException(500, $e->getMessage(), $e);
|
||||
}
|
||||
|
||||
try {
|
||||
$rating = $this->ratingsService->ratingBySku($sku);
|
||||
if (0 === $rating['avg_rating']) {
|
||||
// not rated yet
|
||||
$this->ratingsService->addRatingForSKU($sku, $score);
|
||||
} else {
|
||||
// iffy maths
|
||||
$newAvg = (($rating['avg_rating'] * $rating['rating_count']) + $score) / ($rating['rating_count'] + 1);
|
||||
$this->ratingsService->updateRatingForSKU($sku, $newAvg, $rating['rating_count'] + 1);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException(500, 'Unable to update rating', $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/fetch/{sku}", methods={"GET"})
|
||||
*/
|
||||
public function get(Request $request, string $sku): Response
|
||||
{
|
||||
try {
|
||||
if (!$this->ratingsService->ratingBySku($sku)) {
|
||||
throw new NotFoundHttpException("$sku not found");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException(500, $e->getMessage(), $e);
|
||||
}
|
||||
|
||||
return new JsonResponse($this->ratingsService->ratingBySku($sku));
|
||||
}
|
||||
}
|
55
ratings/html/src/Database.php
Normal file
55
ratings/html/src/Database.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class Database implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $dsn;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $password;
|
||||
|
||||
public function __construct(string $dsn, string $user, string $password)
|
||||
{
|
||||
$this->dsn = $dsn;
|
||||
$this->user = $user;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public function getConnection(): PDO
|
||||
{
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
return new PDO($this->dsn, $this->user, $this->password, $opt);
|
||||
} catch (PDOException $e) {
|
||||
$msg = $e->getMessage();
|
||||
$this->logger->error("Database error $msg");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
42
ratings/html/src/EventListener/InstanaDataCenterListener.php
Normal file
42
ratings/html/src/EventListener/InstanaDataCenterListener.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Instana\RobotShop\Ratings\EventListener;
|
||||
|
||||
use Instana\InstanaRuntimeException;
|
||||
use Instana\Tracer;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class InstanaDataCenterListener
|
||||
{
|
||||
private static $dataCenters = [
|
||||
"asia-northeast2",
|
||||
"asia-south1",
|
||||
"europe-west3",
|
||||
"us-east1",
|
||||
"us-west1"
|
||||
];
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
public function __construct(LoggerInterface $logger)
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
try {
|
||||
$entry = Tracer::getEntrySpan();
|
||||
|
||||
$dataCenter = self::$dataCenters[array_rand(self::$dataCenters)];
|
||||
$entry->annotate('datacenter', $dataCenter);
|
||||
|
||||
$this->logger->info(sprintf('Annotated DataCenter %s', $dataCenter));
|
||||
} catch (InstanaRuntimeException $exception) {
|
||||
$this->logger->error('Unable to annotate entry span: %s', $exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Integration;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Contracts\Service\ResetInterface;
|
||||
|
||||
class InstanaHeadersLoggingProcessor implements EventSubscriberInterface, ResetInterface
|
||||
{
|
||||
private $routeData;
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['addHeaderData', 1],
|
||||
KernelEvents::FINISH_REQUEST => ['removeHeaderData', 1],
|
||||
];
|
||||
}
|
||||
|
||||
public function __invoke(array $records): array
|
||||
{
|
||||
if ($this->routeData && !isset($records['extra']['requests'])) {
|
||||
$records['extra']['instana'] = array_values($this->routeData);
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
public function addHeaderData(RequestEvent $event): void
|
||||
{
|
||||
if ($event->isMasterRequest()) {
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
if (null === $request->headers->get('X-INSTANA-L')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currentTraceHeaders = [
|
||||
'l' => $request->headers->get('X-INSTANA-L', 'n/a'),
|
||||
's' => $request->headers->get('X-INSTANA-S', 'n/a'),
|
||||
't' => $request->headers->get('X-INSTANA-T', 'n/a'),
|
||||
];
|
||||
|
||||
if (null !== $request->headers->get('X-INSTANA-SYNTHETIC')) {
|
||||
$currentTraceHeaders['sy'] = $request->headers->get('X-INSTANA-SYNTHETIC');
|
||||
}
|
||||
|
||||
$this->routeData[spl_object_id($request)] = $currentTraceHeaders;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->routeData = [];
|
||||
}
|
||||
|
||||
public function removeHeaderData(FinishRequestEvent $event): void
|
||||
{
|
||||
$requestId = spl_object_id($event->getRequest());
|
||||
unset($this->routeData[$requestId]);
|
||||
}
|
||||
}
|
136
ratings/html/src/Kernel.php
Normal file
136
ratings/html/src/Kernel.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings;
|
||||
|
||||
use Instana\RobotShop\Ratings\Controller\HealthController;
|
||||
use Instana\RobotShop\Ratings\Controller\RatingsApiController;
|
||||
use Instana\RobotShop\Ratings\EventListener\InstanaDataCenterListener;
|
||||
use Instana\RobotShop\Ratings\Integration\InstanaHeadersLoggingProcessor;
|
||||
use Instana\RobotShop\Ratings\Service\CatalogueService;
|
||||
use Instana\RobotShop\Ratings\Service\HealthCheckService;
|
||||
use Instana\RobotShop\Ratings\Service\RatingsService;
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||
use Symfony\Bundle\MonologBundle\MonologBundle;
|
||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\RouteCollectionBuilder;
|
||||
|
||||
class Kernel extends BaseKernel implements EventSubscriberInterface
|
||||
{
|
||||
use MicroKernelTrait;
|
||||
|
||||
public function registerBundles()
|
||||
{
|
||||
return [
|
||||
new FrameworkBundle(),
|
||||
new MonologBundle(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
KernelEvents::RESPONSE => 'corsResponseFilter',
|
||||
];
|
||||
}
|
||||
|
||||
public function corsResponseFilter(ResponseEvent $event)
|
||||
{
|
||||
$response = $event->getResponse();
|
||||
|
||||
$response->headers->add([
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Access-Control-Allow-Methods' => '*',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void
|
||||
{
|
||||
$c->loadFromExtension('framework', [
|
||||
'secret' => 'S0ME_SECRET',
|
||||
]);
|
||||
|
||||
$c->loadFromExtension('monolog', [
|
||||
'handlers' => [
|
||||
'stdout' => [
|
||||
'type' => 'stream',
|
||||
'level' => 'info',
|
||||
'path' => 'php://stdout',
|
||||
'channels' => ['!request'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$c->setParameter('catalogueUrl', getenv('CATALOGUE_URL') ?: 'http://catalogue:8080');
|
||||
$c->setParameter('pdo_dsn', getenv('PDO_URL') ?: 'mysql:host=mysql;dbname=ratings;charset=utf8mb4');
|
||||
$c->setParameter('pdo_user', 'ratings');
|
||||
$c->setParameter('pdo_password', 'iloveit');
|
||||
$c->setParameter('logger.name', 'RatingsAPI');
|
||||
|
||||
$c->register(InstanaHeadersLoggingProcessor::class)
|
||||
->addTag('kernel.event_subscriber')
|
||||
->addTag('monolog.processor');
|
||||
|
||||
$c->register('monolog.formatter.instana_headers', LineFormatter::class)
|
||||
->addArgument('[%%datetime%%] [%%extra.token%%] %%channel%%.%%level_name%%: %%message%% %%context%% %%extra%%\n');
|
||||
|
||||
$c->register(Database::class)
|
||||
->addArgument($c->getParameter('pdo_dsn'))
|
||||
->addArgument($c->getParameter('pdo_user'))
|
||||
->addArgument($c->getParameter('pdo_password'))
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(CatalogueService::class)
|
||||
->addArgument($c->getParameter('catalogueUrl'))
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(HealthCheckService::class)
|
||||
->addArgument(new Reference('database.connection'))
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register('database.connection', \PDO::class)
|
||||
->setFactory([new Reference(Database::class), 'getConnection']);
|
||||
|
||||
$c->setAlias(\PDO::class, 'database.connection');
|
||||
|
||||
$c->register(RatingsService::class)
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(HealthController::class)
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->addTag('controller.service_arguments')
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(RatingsApiController::class)
|
||||
->addMethodCall('setLogger', [new Reference('logger')])
|
||||
->addTag('controller.service_arguments')
|
||||
->setAutowired(true);
|
||||
|
||||
$c->register(InstanaDataCenterListener::class)
|
||||
->addTag('kernel.event_listener', [
|
||||
'event' => 'kernel.request'
|
||||
])
|
||||
->setAutowired(true);
|
||||
}
|
||||
|
||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||
{
|
||||
$routes->import(__DIR__.'/Controller/', '/', 'annotation');
|
||||
}
|
||||
}
|
48
ratings/html/src/Service/CatalogueService.php
Normal file
48
ratings/html/src/Service/CatalogueService.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Service;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class CatalogueService implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $catalogueUrl;
|
||||
|
||||
public function __construct(string $catalogueUrl)
|
||||
{
|
||||
$this->catalogueUrl = $catalogueUrl;
|
||||
}
|
||||
|
||||
public function checkSKU(string $sku): bool
|
||||
{
|
||||
$url = sprintf('%s/product/%s', $this->catalogueUrl, $sku);
|
||||
|
||||
$opt = [
|
||||
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');
|
||||
}
|
||||
|
||||
$status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$this->logger->info("catalogue status $status");
|
||||
|
||||
curl_close($curl);
|
||||
|
||||
return 200 === $status;
|
||||
}
|
||||
}
|
29
ratings/html/src/Service/HealthCheckService.php
Normal file
29
ratings/html/src/Service/HealthCheckService.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Service;
|
||||
|
||||
use PDO;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class HealthCheckService implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var PDO
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function checkConnectivity(): bool
|
||||
{
|
||||
return $this->pdo->prepare('SELECT 1 + 1 FROM DUAL;')->execute();
|
||||
}
|
||||
}
|
67
ratings/html/src/Service/RatingsService.php
Normal file
67
ratings/html/src/Service/RatingsService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Instana\RobotShop\Ratings\Service;
|
||||
|
||||
use PDO;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
|
||||
class RatingsService implements LoggerAwareInterface
|
||||
{
|
||||
private const QUERY_RATINGS_BY_SKU = 'select avg_rating, rating_count from ratings where sku = ?';
|
||||
private const QUERY_UPDATE_RATINGS_BY_SKU = 'update ratings set avg_rating = ?, rating_count = ? where sku = ?';
|
||||
private const QUERY_INSERT_RATING = 'insert into ratings(sku, avg_rating, rating_count) values(?, ?, ?)';
|
||||
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var PDO
|
||||
*/
|
||||
private $connection;
|
||||
|
||||
public function __construct(PDO $connection)
|
||||
{
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function ratingBySku(string $sku): array
|
||||
{
|
||||
$stmt = $this->connection->prepare(self::QUERY_RATINGS_BY_SKU);
|
||||
if (false === $stmt->execute([$sku])) {
|
||||
$this->logger->error('failed to query data');
|
||||
|
||||
throw new \Exception('Failed to query data', 500);
|
||||
}
|
||||
|
||||
$data = $stmt->fetch();
|
||||
if ($data) {
|
||||
// for some reason avg_rating is return as a string
|
||||
$data['avg_rating'] = (float) $data['avg_rating'];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
// nicer to return an empty record than throw 404
|
||||
return ['avg_rating' => 0, 'rating_count' => 0];
|
||||
}
|
||||
|
||||
public function updateRatingForSKU(string $sku, $score, int $count): void
|
||||
{
|
||||
$stmt = $this->connection->prepare(self::QUERY_UPDATE_RATINGS_BY_SKU);
|
||||
if (!$stmt->execute([$score, $count, $sku])) {
|
||||
$this->logger->error('failed to update rating');
|
||||
throw new \Exception('Failed to update data', 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function addRatingForSKU($sku, $rating): void
|
||||
{
|
||||
$stmt = $this->connection->prepare(self::QUERY_INSERT_RATING);
|
||||
if (!$stmt->execute([$sku, $rating, 1])) {
|
||||
$this->logger->error('failed to insert data');
|
||||
throw new \Exception('Failed to insert data', 500);
|
||||
}
|
||||
}
|
||||
}
|
0
ratings/html/var/cache/.gitkeep
vendored
Normal file
0
ratings/html/var/cache/.gitkeep
vendored
Normal file
0
ratings/html/var/log/.gitkeep
Normal file
0
ratings/html/var/log/.gitkeep
Normal file
4
shipping/.gitignore
vendored
Normal file
4
shipping/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/target
|
||||
/.classpath
|
||||
/.project
|
||||
/.settings
|
@@ -1,15 +1,14 @@
|
||||
#
|
||||
# Build
|
||||
#
|
||||
FROM openjdk:8-jdk AS build
|
||||
FROM debian:10 AS build
|
||||
|
||||
RUN apt-get update && apt-get -y install maven
|
||||
|
||||
WORKDIR /opt/shipping
|
||||
|
||||
COPY pom.xml /opt/shipping/
|
||||
RUN mvn install
|
||||
|
||||
RUN mvn dependency:resolve
|
||||
COPY src /opt/shipping/src/
|
||||
RUN mvn package
|
||||
|
||||
@@ -25,7 +24,7 @@ WORKDIR /opt/shipping
|
||||
ENV CART_ENDPOINT=cart:8080
|
||||
ENV DB_HOST=mysql
|
||||
|
||||
COPY --from=build /opt/shipping/target/shipping-1.0-jar-with-dependencies.jar shipping.jar
|
||||
COPY --from=build /opt/shipping/target/shipping-1.0.jar shipping.jar
|
||||
|
||||
CMD [ "java", "-Xmn256m", "-Xmx768m", "-jar", "shipping.jar" ]
|
||||
|
||||
|
132
shipping/pom.xml
132
shipping/pom.xml
@@ -1,78 +1,78 @@
|
||||
<project>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>steveww</groupId>
|
||||
<artifactId>shipping</artifactId>
|
||||
<version>1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spark Java Sample</name>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.3.3.RELEASE</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.instana</groupId>
|
||||
<artifactId>shipping</artifactId>
|
||||
<version>1.0</version>
|
||||
<name>shipping service</name>
|
||||
<description>Shipping calculations</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sparkjava</groupId>
|
||||
<artifactId>spark-core</artifactId>
|
||||
<version>2.7.2</version>
|
||||
<groupId>org.springframework.retry</groupId>
|
||||
<artifactId>spring-retry</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>1.7.25</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>c3p0</groupId>
|
||||
<artifactId>c3p0</artifactId>
|
||||
<version>0.9.1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>5.1.45</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-dbutils</groupId>
|
||||
<artifactId>commons-dbutils</artifactId>
|
||||
<version>1.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.8.2</version>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.instana</groupId>
|
||||
<artifactId>instana-java-sdk</artifactId>
|
||||
<version>1.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.5.5</version>
|
||||
<version>4.5.12</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>org.steveww.spark.Main</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
@@ -1,23 +1,20 @@
|
||||
package org.steveww.spark;
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
public class Location {
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
|
||||
public Location(double latitude, double longitude) {
|
||||
public class Calculator {
|
||||
private double latitude = 0;
|
||||
private double longitude = 0;
|
||||
|
||||
Calculator(double latitdue, double longitude) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return this.latitude;
|
||||
Calculator(City city) {
|
||||
this.latitude = city.getLatitude();
|
||||
this.longitude = city.getLongitude();
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return this.longitude;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the distance between this location and the target location.
|
||||
* Use decimal lat/long degrees
|
||||
@@ -34,10 +31,13 @@ public class Location {
|
||||
double diffLatR = Math.toRadians(targetLatitude - this.latitude);
|
||||
double diffLongR = Math.toRadians(targetLongitude - this.longitude);
|
||||
|
||||
double a = Math.sin(diffLatR / 2.0) * Math.sin(diffLatR / 2.0) + Math.cos(latitudeR) * Math.cos(targetLatitudeR) * Math.sin(diffLongR / 2.0) * Math.sin(diffLongR);
|
||||
double a = Math.sin(diffLatR / 2.0) * Math.sin(diffLatR / 2.0)
|
||||
+ Math.cos(latitudeR) * Math.cos(targetLatitudeR)
|
||||
* Math.sin(diffLongR / 2.0) * Math.sin(diffLongR);
|
||||
|
||||
double c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1.0 - a));
|
||||
|
||||
return (long)Math.rint(earthRadius * c / 1000.0);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,75 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
import org.apache.http.params.HttpParams;
|
||||
|
||||
public class CartHelper {
|
||||
private static final Logger logger = LoggerFactory.getLogger(CartHelper.class);
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
public CartHelper(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
// TODO - Remove deprecated calls
|
||||
public String addToCart(String id, String data) {
|
||||
logger.info("add shipping to cart {}", id);
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
|
||||
CloseableHttpClient httpClient = null;
|
||||
try {
|
||||
// set timeout to 5 secs
|
||||
HttpParams httpParams = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
|
||||
|
||||
httpClient = HttpClients.createDefault();
|
||||
HttpPost postRequest = new HttpPost(baseUrl + id);
|
||||
StringEntity payload = new StringEntity(data);
|
||||
payload.setContentType("application/json");
|
||||
postRequest.setEntity(payload);
|
||||
CloseableHttpResponse res = httpClient.execute(postRequest);
|
||||
|
||||
if (res.getStatusLine().getStatusCode() == 200) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(res.getEntity().getContent()));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
buffer.append(line);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Failed with code {}", res.getStatusLine().getStatusCode());
|
||||
}
|
||||
try {
|
||||
res.close();
|
||||
} catch(IOException e) {
|
||||
logger.warn("httpresponse", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.warn("http client exception", e);
|
||||
} finally {
|
||||
if (httpClient != null) {
|
||||
try {
|
||||
httpClient.close();
|
||||
} catch(IOException e) {
|
||||
logger.warn("httpclient", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this will be empty on error
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.Column;
|
||||
|
||||
/*
|
||||
* Bean for City
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "cities")
|
||||
public class City {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private long uuid;
|
||||
|
||||
@Column(name = "country_code")
|
||||
private String code;
|
||||
private String city;
|
||||
private String name;
|
||||
private String region;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
|
||||
public long getUuid() {
|
||||
return this.uuid;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCity() {
|
||||
return this.city;
|
||||
}
|
||||
|
||||
public void setCity(String city) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return this.region;
|
||||
}
|
||||
|
||||
public void setRegion(String code) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return this.latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return this.longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Country: %s City: %s Region: %s Coords: %f %f", this.code, this.city, this.region, this.latitude, this.longitude);
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
public interface CityRepository extends CrudRepository<City, Long> {
|
||||
List<City> findByCode(String code);
|
||||
|
||||
@Query(
|
||||
value = "select c from City c where c.code = ?1 and c.city like ?2%"
|
||||
)
|
||||
List<City> match(String code, String text);
|
||||
|
||||
City findById(long id);
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
|
||||
/*
|
||||
* Bean for Code
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "codes")
|
||||
public class Code {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private long uuid;
|
||||
|
||||
private String code;
|
||||
private String name;
|
||||
|
||||
public long getUuid() {
|
||||
return this.uuid;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Code: %s Name: %s", this.code, this.name);
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.repository.PagingAndSortingRepository;
|
||||
|
||||
public interface CodeRepository extends PagingAndSortingRepository<Code, Long> {
|
||||
|
||||
Iterable<Code> findAll();
|
||||
|
||||
Code findById(long id);
|
||||
}
|
@@ -0,0 +1,145 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@RestController
|
||||
public class Controller {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Controller.class);
|
||||
|
||||
private String CART_URL = String.format("http://%s/shipping/", getenv("CART_ENDPOINT", "cart"));
|
||||
|
||||
public static List bytesGlobal = Collections.synchronizedList(new ArrayList<byte[]>());
|
||||
|
||||
@Autowired
|
||||
private CityRepository cityrepo;
|
||||
|
||||
@Autowired
|
||||
private CodeRepository coderepo;
|
||||
|
||||
private String getenv(String key, String def) {
|
||||
String val = System.getenv(key);
|
||||
val = val == null ? def : val;
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
@GetMapping(path = "/memory")
|
||||
public int memory() {
|
||||
byte[] bytes = new byte[1024 * 1024 * 25];
|
||||
Arrays.fill(bytes,(byte)8);
|
||||
bytesGlobal.add(bytes);
|
||||
|
||||
return bytesGlobal.size();
|
||||
}
|
||||
|
||||
@GetMapping(path = "/free")
|
||||
public int free() {
|
||||
bytesGlobal.clear();
|
||||
|
||||
return bytesGlobal.size();
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
public String health() {
|
||||
return "OK";
|
||||
}
|
||||
|
||||
@GetMapping("/count")
|
||||
public String count() {
|
||||
long count = cityrepo.count();
|
||||
|
||||
return String.valueOf(count);
|
||||
}
|
||||
|
||||
@GetMapping("/codes")
|
||||
public Iterable<Code> codes() {
|
||||
logger.info("all codes");
|
||||
|
||||
Iterable<Code> codes = coderepo.findAll(Sort.by(Sort.Direction.ASC, "name"));
|
||||
|
||||
return codes;
|
||||
}
|
||||
|
||||
@GetMapping("/cities/{code}")
|
||||
public List<City> cities(@PathVariable String code) {
|
||||
logger.info("cities by code {}", code);
|
||||
|
||||
List<City> cities = cityrepo.findByCode(code);
|
||||
|
||||
return cities;
|
||||
}
|
||||
|
||||
@GetMapping("/match/{code}/{text}")
|
||||
public List<City> match(@PathVariable String code, @PathVariable String text) {
|
||||
logger.info("match code {} text {}", code, text);
|
||||
|
||||
if (text.length() < 3) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
List<City> cities = cityrepo.match(code, text);
|
||||
/*
|
||||
* This is a dirty hack to limit the result size
|
||||
* I'm sure there is a more spring boot way to do this
|
||||
* TODO - neater
|
||||
*/
|
||||
if (cities.size() > 10) {
|
||||
cities = cities.subList(0, 9);
|
||||
}
|
||||
|
||||
return cities;
|
||||
}
|
||||
|
||||
@GetMapping("/calc/{id}")
|
||||
public Ship caclc(@PathVariable long id) {
|
||||
double homeLatitude = 51.164896;
|
||||
double homeLongitude = 7.068792;
|
||||
|
||||
logger.info("Calculation for {}", id);
|
||||
|
||||
City city = cityrepo.findById(id);
|
||||
if (city == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "city not found");
|
||||
}
|
||||
|
||||
Calculator calc = new Calculator(city);
|
||||
long distance = calc.getDistance(homeLatitude, homeLongitude);
|
||||
// avoid rounding
|
||||
double cost = Math.rint(distance * 5) / 100.0;
|
||||
Ship ship = new Ship(distance, cost);
|
||||
logger.info("shipping {}", ship);
|
||||
|
||||
return ship;
|
||||
}
|
||||
|
||||
// enforce content type
|
||||
@PostMapping(path = "/confirm/{id}", consumes = "application/json", produces = "application/json")
|
||||
public String confirm(@PathVariable String id, @RequestBody String body) {
|
||||
logger.info("confirm id: {}", id);
|
||||
logger.info("body {}", body);
|
||||
|
||||
CartHelper helper = new CartHelper(CART_URL);
|
||||
String cart = helper.addToCart(id, body);
|
||||
|
||||
if (cart.equals("")) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "cart not found");
|
||||
}
|
||||
|
||||
return cart;
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import javax.sql.DataSource;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@Configuration
|
||||
public class JpaConfig {
|
||||
private static final Logger logger = LoggerFactory.getLogger(JpaConfig.class);
|
||||
|
||||
@Bean
|
||||
public DataSource getDataSource() {
|
||||
String JDBC_URL = String.format("jdbc:mysql://%s/cities?useSSL=false&autoReconnect=true", System.getenv("DB_HOST") == null ? "mysql" : System.getenv("DB_HOST"));
|
||||
|
||||
logger.info("jdbc url {}", JDBC_URL);
|
||||
|
||||
DataSourceBuilder bob = DataSourceBuilder.create();
|
||||
|
||||
bob.driverClassName("com.mysql.jdbc.Driver");
|
||||
bob.url(JDBC_URL);
|
||||
bob.username("shipping");
|
||||
bob.password("secret");
|
||||
|
||||
return bob.build();
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.jdbc.datasource.AbstractDataSource;
|
||||
import org.springframework.retry.annotation.Retryable;
|
||||
import org.springframework.retry.annotation.Backoff;
|
||||
|
||||
class RetryableDataSource extends AbstractDataSource {
|
||||
private DataSource delegate;
|
||||
|
||||
public RetryableDataSource(DataSource delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retryable(maxAttempts = 10, backoff = @Backoff(multiplier = 2.3, maxDelay = 30000))
|
||||
public Connection getConnection() throws SQLException {
|
||||
return delegate.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retryable(maxAttempts = 10, backoff = @Backoff(multiplier = 2.3, maxDelay = 30000))
|
||||
public Connection getConnection(String username, String password) throws SQLException {
|
||||
return delegate.getConnection(username, password);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package org.steveww.spark;
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
/**
|
||||
* Bean to hold shipping information
|
||||
@@ -12,7 +12,7 @@ public class Ship {
|
||||
this.cost = 0.0;
|
||||
}
|
||||
|
||||
public Ship(long distnace, double cost) {
|
||||
public Ship(long distance, double cost) {
|
||||
this.distance = distance;
|
||||
this.cost = cost;
|
||||
}
|
||||
@@ -32,5 +32,10 @@ public class Ship {
|
||||
public double getCost() {
|
||||
return this.cost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("Distance: %d Cost: %f", distance, cost);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,76 @@
|
||||
package com.instana.robotshop.shipping;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import com.instana.sdk.support.SpanSupport;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.retry.annotation.EnableRetry;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableRetry
|
||||
@EnableWebMvc
|
||||
public class ShippingServiceApplication implements WebMvcConfigurer {
|
||||
|
||||
private static final String[] DATA_CENTERS = {
|
||||
"asia-northeast2",
|
||||
"asia-south1",
|
||||
"europe-west3",
|
||||
"us-east1",
|
||||
"us-west1"
|
||||
};
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ShippingServiceApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BeanPostProcessor dataSourceWrapper() {
|
||||
return new DataSourcePostProcessor();
|
||||
}
|
||||
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
private static class DataSourcePostProcessor implements BeanPostProcessor {
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
|
||||
if (bean instanceof DataSource) {
|
||||
bean = new RetryableDataSource((DataSource)bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String name) throws BeansException {
|
||||
return bean;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new InstanaDatacenterTagInterceptor());
|
||||
}
|
||||
|
||||
private static class InstanaDatacenterTagInterceptor extends HandlerInterceptorAdapter {
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
|
||||
SpanSupport.annotate("datacenter", DATA_CENTERS[new Random().nextInt(DATA_CENTERS.length)]);
|
||||
|
||||
return super.preHandle(request, response, handler);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,252 +0,0 @@
|
||||
package org.steveww.spark;
|
||||
|
||||
import com.mchange.v2.c3p0.ComboPooledDataSource;
|
||||
import spark.Spark;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
import org.apache.http.params.BasicHttpParams;
|
||||
import org.apache.http.params.HttpConnectionParams;
|
||||
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;
|
||||
|
||||
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");
|
||||
|
||||
//
|
||||
// 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");
|
||||
// some config
|
||||
cpds.setMinPoolSize(5);
|
||||
cpds.setAcquireIncrement(5);
|
||||
cpds.setMaxPoolSize(20);
|
||||
cpds.setMaxStatements(180);
|
||||
}
|
||||
catch(Exception e) {
|
||||
logger.error("Database Exception", e);
|
||||
}
|
||||
|
||||
// Spark
|
||||
Spark.port(8080);
|
||||
|
||||
Spark.get("/health", (req, res) -> "OK");
|
||||
|
||||
Spark.get("/count", (req, res) -> {
|
||||
String data;
|
||||
try {
|
||||
data = queryToJson("select count(*) as count from cities");
|
||||
res.header("Content-Type", "application/json");
|
||||
} catch(Exception e) {
|
||||
logger.error("count", e);
|
||||
res.status(500);
|
||||
data = "ERROR";
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
Spark.get("/codes", (req, res) -> {
|
||||
String data;
|
||||
try {
|
||||
String query = "select code, name from codes order by name asc";
|
||||
data = queryToJson(query);
|
||||
res.header("Content-Type", "application/json");
|
||||
} catch(Exception e) {
|
||||
logger.error("codes", e);
|
||||
res.status(500);
|
||||
data = "ERROR";
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
// needed for load gen script
|
||||
Spark.get("/cities/:code", (req, res) -> {
|
||||
String data;
|
||||
try {
|
||||
String query = "select uuid, name from cities where country_code = ?";
|
||||
logger.info("Query " + query);
|
||||
data = queryToJson(query, req.params(":code"));
|
||||
res.header("Content-Type", "application/json");
|
||||
} catch(Exception e) {
|
||||
logger.error("cities", e);
|
||||
res.status(500);
|
||||
data = "ERROR";
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
Spark.get("/match/:code/:text", (req, res) -> {
|
||||
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);
|
||||
data = queryToJson(query, req.params(":code"), req.params(":text") + "%");
|
||||
res.header("Content-Type", "application/json");
|
||||
} catch(Exception e) {
|
||||
logger.error("match", e);
|
||||
res.status(500);
|
||||
data = "ERROR";
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
Spark.get("/calc/:uuid", (req, res) -> {
|
||||
double homeLat = 51.164896;
|
||||
double homeLong = 7.068792;
|
||||
String data;
|
||||
|
||||
Location location = getLocation(req.params(":uuid"));
|
||||
Ship ship = new Ship();
|
||||
if(location != null) {
|
||||
long distance = location.getDistance(homeLat, homeLong);
|
||||
// charge 0.05 Euro per km
|
||||
// try to avoid rounding errors
|
||||
double cost = Math.rint(distance * 5) / 100.0;
|
||||
ship.setDistance(distance);
|
||||
ship.setCost(cost);
|
||||
res.header("Content-Type", "application/json");
|
||||
data = new Gson().toJson(ship);
|
||||
} else {
|
||||
data = "no location";
|
||||
logger.warn(data);
|
||||
res.status(400);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
if(cart.equals("")) {
|
||||
res.status(404);
|
||||
} else {
|
||||
res.header("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
return cart;
|
||||
});
|
||||
|
||||
logger.info("Ready");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Query to Json - QED
|
||||
**/
|
||||
private static String queryToJson(String query, Object ... args) {
|
||||
List<Map<String, Object>> listOfMaps = null;
|
||||
try {
|
||||
QueryRunner queryRunner = new QueryRunner(cpds);
|
||||
listOfMaps = queryRunner.query(query, new MapListHandler(), args);
|
||||
} catch (SQLException se) {
|
||||
throw new RuntimeException("Couldn't query the database.", se);
|
||||
}
|
||||
|
||||
return new Gson().toJson(listOfMaps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case for location, dont want Json
|
||||
**/
|
||||
private static Location getLocation(String uuid) {
|
||||
Location location = null;
|
||||
Connection conn = null;
|
||||
PreparedStatement stmt = null;
|
||||
ResultSet rs = null;
|
||||
String query = "select latitude, longitude from cities where uuid = ?";
|
||||
|
||||
try {
|
||||
conn = cpds.getConnection();
|
||||
stmt = conn.prepareStatement(query);
|
||||
stmt.setInt(1, Integer.parseInt(uuid));
|
||||
rs = stmt.executeQuery();
|
||||
while(rs.next()) {
|
||||
location = new Location(rs.getDouble(1), rs.getDouble(2));
|
||||
break;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.error("Location exception", e);
|
||||
} finally {
|
||||
DbUtils.closeQuietly(conn, stmt, rs);
|
||||
}
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
private static String addToCart(String id, String data) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
|
||||
DefaultHttpClient httpClient = null;
|
||||
try {
|
||||
// set timeout to 5 secs
|
||||
HttpParams httpParams = new BasicHttpParams();
|
||||
HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
|
||||
|
||||
httpClient = new DefaultHttpClient(httpParams);
|
||||
HttpPost postRequest = new HttpPost(CART_URL + id);
|
||||
StringEntity payload = new StringEntity(data);
|
||||
payload.setContentType("application/json");
|
||||
postRequest.setEntity(payload);
|
||||
HttpResponse res = httpClient.execute(postRequest);
|
||||
|
||||
if(res.getStatusLine().getStatusCode() == 200) {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(res.getEntity().getContent()));
|
||||
String line;
|
||||
while((line = in.readLine()) != null) {
|
||||
buffer.append(line);
|
||||
}
|
||||
} else {
|
||||
logger.warn("Failed with code: " + res.getStatusLine().getStatusCode());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
logger.error("http client exception", e);
|
||||
} finally {
|
||||
if(httpClient != null) {
|
||||
httpClient.getConnectionManager().shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
6
shipping/src/main/resources/application.properties
Normal file
6
shipping/src/main/resources/application.properties
Normal file
@@ -0,0 +1,6 @@
|
||||
spring.jmx.enabled=true
|
||||
|
||||
management.endpoint.info.enabled=true
|
||||
management.endpoint.health.enabled=true
|
||||
management.endpoint.metrics.enabled=true
|
||||
management.endpoint.env.enabled=true
|
@@ -1,4 +1,6 @@
|
||||
FROM node:10
|
||||
FROM node:14
|
||||
|
||||
ENV INSTANA_AUTO_PROFILE true
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
@@ -16,6 +16,6 @@
|
||||
"pino": "^5.10.8",
|
||||
"express-pino-logger": "^4.0.0",
|
||||
"pino-pretty": "^2.5.0",
|
||||
"@instana/collector": "^1.90.0"
|
||||
"@instana/collector": "^1.132.2"
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,20 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
let dcs = [
|
||||
"asia-northeast2",
|
||||
"asia-south1",
|
||||
"europe-west3",
|
||||
"us-east1",
|
||||
"us-west1"
|
||||
];
|
||||
let span = instana.currentSpan();
|
||||
span.annotate('custom.sdk.tags.datacenter', dcs[Math.floor(Math.random() * dcs.length)]);
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
@@ -54,7 +68,6 @@ app.get('/health', (req, res) => {
|
||||
|
||||
// use REDIS INCR to track anonymous users
|
||||
app.get('/uniqueid', (req, res) => {
|
||||
req.log.error('Unique ID test');
|
||||
// get number from Redis
|
||||
redisClient.incr('anonymous-counter', (err, r) => {
|
||||
if(!err) {
|
||||
|
@@ -1,4 +1,23 @@
|
||||
FROM nginx:1.16
|
||||
FROM alpine AS build
|
||||
ARG KEY
|
||||
|
||||
WORKDIR /instana
|
||||
|
||||
RUN apk add --update --no-cache curl
|
||||
|
||||
RUN if [ -n "$KEY" ]; then curl \
|
||||
--output instana.zip \
|
||||
--user "_:$KEY" \
|
||||
https://artifact-public.instana.io/artifactory/shared/com/instana/nginx_tracing/1.1.2/linux-amd64-glibc-nginx-1.20.1.zip && \
|
||||
unzip instana.zip && \
|
||||
mv glibc-libinstana_sensor.so libinstana_sensor.so && \
|
||||
mv glibc-nginx-1.20.1-ngx_http_ot_module.so ngx_http_opentracing_module.so; \
|
||||
else echo "KEY not provided. Not adding tracing"; \
|
||||
touch dummy.so; \
|
||||
fi
|
||||
|
||||
|
||||
FROM nginx:1.20.1
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
@@ -7,11 +26,14 @@ ENV CATALOGUE_HOST=catalogue \
|
||||
CART_HOST=cart \
|
||||
SHIPPING_HOST=shipping \
|
||||
PAYMENT_HOST=payment \
|
||||
RATINGS_HOST=ratings
|
||||
RATINGS_HOST=ratings \
|
||||
INSTANA_SERVICE_NAME=nginx-web
|
||||
|
||||
# Instana tracing
|
||||
COPY --from=build /instana/*.so /tmp/
|
||||
|
||||
COPY entrypoint.sh /root/
|
||||
ENTRYPOINT ["/root/entrypoint.sh"]
|
||||
|
||||
COPY default.conf.template /etc/nginx/conf.d/default.conf.template
|
||||
COPY static /usr/share/nginx/html
|
||||
|
||||
|
@@ -1,3 +1,7 @@
|
||||
# Instana tracing
|
||||
opentracing_load_tracer /usr/local/lib/libinstana_sensor.so /etc/instana-config.json;
|
||||
opentracing_propagate_context;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
@@ -18,7 +22,7 @@ server {
|
||||
location /images/ {
|
||||
expires 5s;
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri /images/placeholder.jpg;
|
||||
try_files $uri /images/placeholder.png;
|
||||
}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
@@ -82,4 +86,3 @@ server {
|
||||
access_log off;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#set -x
|
||||
# set -x
|
||||
|
||||
# echo "arg 1 $1"
|
||||
|
||||
@@ -15,8 +15,10 @@ if [ -n "$INSTANA_EUM_KEY" -a -n "$INSTANA_EUM_REPORTING_URL" ]
|
||||
then
|
||||
echo "Enabling Instana EUM"
|
||||
# use | instead of / as command delimiter to avoid eacaping the url
|
||||
# strip off any trailing /
|
||||
SAFE_URL=$(echo "$INSTANA_EUM_REPORTING_URL" | sed 's|/*$||')
|
||||
sed -i "s|INSTANA_EUM_KEY|$INSTANA_EUM_KEY|" $BASE_DIR/eum-tmpl.html
|
||||
sed -i "s|INSTANA_EUM_REPORTING_URL|$INSTANA_EUM_REPORTING_URL|" $BASE_DIR/eum-tmpl.html
|
||||
sed -i "s|INSTANA_EUM_REPORTING_URL|$SAFE_URL|" $BASE_DIR/eum-tmpl.html
|
||||
cp $BASE_DIR/eum-tmpl.html $BASE_DIR/eum.html
|
||||
else
|
||||
echo "EUM not enabled"
|
||||
@@ -29,5 +31,30 @@ chmod 644 $BASE_DIR/eum.html
|
||||
# apply environment variables to default.conf
|
||||
envsubst '${CATALOGUE_HOST} ${USER_HOST} ${CART_HOST} ${SHIPPING_HOST} ${PAYMENT_HOST} ${RATINGS_HOST}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
if [ -f /tmp/ngx_http_opentracing_module.so -a -f /tmp/libinstana_sensor.so ]
|
||||
then
|
||||
echo "Patching for Instana tracing"
|
||||
mv /tmp/ngx_http_opentracing_module.so /usr/lib/nginx/modules
|
||||
mv /tmp/libinstana_sensor.so /usr/local/lib
|
||||
cat - /etc/nginx/nginx.conf << !EOF! > /tmp/nginx.conf
|
||||
# Extra configuration for Instana tracing
|
||||
load_module modules/ngx_http_opentracing_module.so;
|
||||
|
||||
# Pass through these env vars
|
||||
env INSTANA_SERVICE_NAME;
|
||||
env INSTANA_AGENT_HOST;
|
||||
env INSTANA_AGENT_PORT;
|
||||
env INSTANA_MAX_BUFFERED_SPANS;
|
||||
env INSTANA_DEV;
|
||||
!EOF!
|
||||
|
||||
mv /tmp/nginx.conf /etc/nginx/nginx.conf
|
||||
echo "{}" > /etc/instana-config.json
|
||||
else
|
||||
echo "Tracing not enabled"
|
||||
# remove tracing config
|
||||
sed -i '1,3d' /etc/nginx/conf.d/default.conf
|
||||
fi
|
||||
|
||||
exec nginx-debug -g "daemon off;"
|
||||
|
||||
|
@@ -1,12 +1,13 @@
|
||||
<!-- 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('key', 'INSTANA_EUM_KEY');
|
||||
(function(s,t,a,n){s[t]||(s[t]=a,n=s[a]=function(){n.q.push(arguments)},
|
||||
n.q=[],n.v=2,n.l=1*new Date)})(window,"InstanaEumObject","ineum");
|
||||
|
||||
ineum('reportingUrl', 'INSTANA_EUM_REPORTING_URL');
|
||||
ineum('page', 'splash.html');
|
||||
ineum('key', 'INSTANA_EUM_KEY');
|
||||
ineum('trackSessions');
|
||||
ineum('page', 'splash');
|
||||
</script>
|
||||
<script defer crossorigin="anonymous" src="INSTANA_EUM_REPORTING_URL/eum.min.js"></script>
|
||||
<!-- EUM include end -->
|
||||
|
||||
|
BIN
web/static/images/Aplha.png
Normal file
BIN
web/static/images/Aplha.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user