117 Commits

Author SHA1 Message Date
Teresa Noviello
5edc308df0 Renamed general php call log to calls.log and redirected there only the php-related info. Added rotation to calls.log. Updated the documentation. 2021-11-03 15:31:24 +00:00
Teresa Noviello
18b7dd75c8 Improved the python logging with the import of loggin module. Added documentation for the new behaviours of load-gen.sh and improved the previous descriptions a bit 2021-11-03 14:14:53 +00:00
Teresa Noviello
740d73549d load-gen.sh shouldn't create a placeholder file 2021-11-03 11:12:10 +00:00
Teresa Noviello
ec5babff6e change the name of the php general log 2021-11-03 09:20:50 +00:00
Teresa Noviello
5fa1c41698 Removed from the load gen script the dynamic mount of the python load generator (robot-shop.py) and the entrypoint (entrypoint.sh) 2021-11-02 16:33:50 +00:00
Teresa Noviello
ea7f80d398 Re-added .env with the same info as before 2021-11-02 16:14:06 +00:00
Teresa Noviello
cbe067283c not intended for public repository, removing with BFG repo cleaner 2021-11-02 16:03:10 +00:00
Teresa Noviello
ea6f4c8c10 not intended for public repository, removing with BFG repo cleaner 2021-11-02 16:00:45 +00:00
Teresa Noviello
f774d0d1fd no env file in public repositories, removing with BFG repo cleaner 2021-11-02 15:52:43 +00:00
Teresa Noviello
c6caf5964b Added traces_logger script in order to launch an instana-agent with traces debug, robot shop apps and generate load 2021-11-02 10:16:52 +00:00
Teresa Noviello
e4ec129e4f Added to the load test script documentation and the new option of verbose mode, which triggers also the load debug, at the time only tracking the php service's rating requests. Rating requests are logged into a log file and formatted into a CSV file 2021-11-01 12:11:58 +00:00
SteveWW
b1adf4c650 fix typo 2021-09-01 12:27:45 +01:00
Steve Waterworth
48eeb4cc68 Merge pull request #69 from instana/agent
Agent
2021-08-31 16:29:32 +01:00
SteveWW
31b808e743 update all versions 2021-08-31 16:15:36 +01:00
Steve Waterworth
7316800e1f update agent versions 2021-08-31 12:01:55 +01:00
Steve Waterworth
b5495f847c strip trailing slash from eum url 2021-08-26 10:17:55 +01:00
SteveWW
0328084ad4 conditional build for empty KEY 2021-06-25 12:18:18 +01:00
SteveWW
9d8c34027f bump version 2021-06-25 11:45:05 +01:00
Steve Waterworth
53a67a13cb Merge pull request #67 from instana/nginx
add Nginx tracing
2021-06-25 11:34:07 +01:00
SteveWW
d71c1216ab add Nginx tracing 2021-06-25 11:30:16 +01:00
Steve Waterworth
e80da6d5e0 Merge pull request #66 from instana/ZbigniewZabost-patch-1
Update README.md
2021-06-22 14:39:28 +01:00
Zbigniew Zabost
b2384ee732 Update README.md 2021-06-22 10:51:45 +02:00
Zbigniew Zabost
165e292328 Update README.md 2021-06-22 09:31:06 +02:00
Steve Waterworth
f1becd3bd0 Merge pull request #64 from instana/eum
Eum
2021-06-07 14:58:08 +01:00
Steve Waterworth
eaa36113a0 update comment 2021-06-07 14:56:52 +01:00
Bright Zheng
33a55a08e0 fix CORS error issue 2021-06-04 16:46:39 +08:00
Steve Waterworth
4c5903048c fix perms 2021-05-12 09:53:13 +01:00
Steve Waterworth
aa353caa67 update link for helm chart 2021-04-16 16:18:40 +01:00
Matthias Huber
0b45e47074 Fixed broken link to Instana Agent Helm Repo 2021-04-16 12:52:42 +02:00
Steve Waterworth
9581ee7646 fix rewrite rule 2021-03-24 11:04:07 +00:00
Steve Waterworth
0efbf5ead4 Merge branch 'fluentd' 2021-03-19 10:51:19 +00:00
Steve Waterworth
ccd7b31a7a fix typos 2021-03-19 10:50:34 +00:00
Steve Waterworth
fbd4024d10 minor tweaks 2021-03-19 10:44:42 +00:00
Steve Waterworth
5a22b100b2 reorganise directory 2021-03-19 10:17:14 +00:00
Steve Waterworth
4c03be5d1e initial load 2021-03-17 17:15:23 +00:00
Steve Waterworth
bdc88e6e91 fix missing variable 2021-03-17 15:30:51 +00:00
Steve Waterworth
e90ebb9e33 initial load 2021-03-17 15:24:19 +00:00
Steve Waterworth
ca390c6858 bump version 2021-03-15 09:01:27 +00:00
Steve Waterworth
d1dd589672 Merge branch 'memory' 2021-03-15 08:58:57 +00:00
Steve Waterworth
2cf169500b memory leak 2021-03-15 08:57:52 +00:00
Steve Waterworth
905d185445 Merge pull request #58 from instana/ibm
LGTM
2021-03-02 16:45:16 +00:00
Steve Waterworth
fa602802fa remove opentracing 2021-03-02 16:02:45 +00:00
Steve Waterworth
21afad2b70 new images and descriptions 2021-03-02 12:32:46 +00:00
Dishant Kaushik
71ddff31bc Changed images to match Instana color pallete 2021-03-01 08:35:29 -05:00
Dishant Kaushik
2fcc0c9835 - If users want a route to the web service, added a Route with the service 2021-02-24 14:30:05 -05:00
Dishant Kaushik
db45fae8d6 - removed licensed images
- replaced with new images
- changed the product image to png for consistency.
- changed the names of robots and their SKU.
2021-02-24 14:21:57 -05:00
Jiaxuan-Yang
f07213185f add mem-leak api 2021-02-22 14:35:41 -05:00
Cedric Ziel
a8f89df7dd Merge pull request #52 from instana/composer2-fix
Fix implicit upgrade of composer to version 2
2021-02-11 08:51:59 +01:00
Cedric Ziel
436e7eef7e Merge branch 'master' into composer2-fix 2021-02-11 08:51:54 +01:00
Cedric Ziel
9091749a9d Fix implicit upgrade of composer to version 2 2021-02-11 08:49:57 +01:00
Cedric Ziel
d866400e91 Merge pull request #51 from instana/cedricziel-opcache
Install opcache PHP extension
2021-02-11 08:27:05 +01:00
Cedric Ziel
65be51cb2b Install opcache PHP extension 2021-02-11 08:26:11 +01:00
Nathan Fisher
80616f0090 Add redis.storageClassName to permit an override of the storage class (#50) 2020-12-14 18:14:31 -05:00
Nathan Fisher
d60dc128c4 Add table outlining Helm key values (#49) 2020-12-14 14:55:08 -05:00
Cedric Ziel
e181d9bf07 Merge pull request #47 from instana/logging
Add logging settings to prevent system pollution
2020-11-16 16:06:17 +01:00
Cedric Ziel
4c243455b8 Add logging branches to prevent system pollution 2020-11-16 16:04:49 +01:00
Cedric Ziel
8cd5551984 Remove leftover njs config 2020-11-02 17:47:23 +01:00
Cedric Ziel
d45b613b61 Merge pull request #46 from instana/geo 2020-11-02 13:28:35 +01:00
Cedric Ziel
a5ed9ab94e Send header for leftover calls 2020-11-02 13:27:45 +01:00
Cedric Ziel
80082a58c0 Reduce set of data centers and add some ip addresses 2020-11-02 13:27:29 +01:00
Cedric Ziel
4745f2393c Add datacenter tag to entry-spans
This will add a random "datacenter" tag on the entries where
supported to improve showcasing geo capabilities.
2020-11-02 13:07:19 +01:00
Cedric Ziel
a11b90275c Add x-forwarded-for header in load-gen instead 2020-11-02 13:07:19 +01:00
Cedric Ziel
0ceed6d94f Add docker-compose healthchecks to services that support it 2020-11-02 13:06:16 +01:00
Steve Waterworth
29554c3023 limit image builds 2020-10-22 09:23:55 +01:00
steveww
d7a782b255 Merge pull request #44 from instana/geo
Showcase geo tags for calls derived from x-forwarded-for header. LGTM.
2020-10-21 14:52:12 +01:00
Steve Waterworth
d98d955fae minor optimisations 2020-10-21 12:53:31 +01:00
Cedric Ziel
83ab82407c Showcase geo tags for calls derived from x-forwarded-for header 2020-10-19 11:55:31 +02:00
Cedric Ziel
f0619949bc Enable PHP profiling for ratings service (#43) 2020-10-05 15:39:27 +02:00
steveww
3d9ce2607b Merge pull request #30 from instana/synthetics
Modernize ratings service and send synthetic headers for healthchecks
2020-10-05 12:24:25 +01:00
Cedric Ziel
838f96536c Merge branch 'master' into synthetics 2020-10-05 13:01:24 +02:00
Cedric Ziel
e727811823 Add .gitignore file for shipping service 2020-10-05 12:27:54 +02:00
Steve Waterworth
345ba7561c spring boot 2020-09-01 16:49:43 +01:00
Steve Waterworth
775e380318 bump version 2020-09-01 16:33:07 +01:00
Steve Waterworth
5ef8c91c3c Merge branch 'springboot' 2020-09-01 16:31:58 +01:00
Steve Waterworth
f0b49233a3 spring boot application 2020-09-01 16:30:35 +01:00
Steve Waterworth
86bcdd57c2 preloading data no longer works 2020-09-01 16:25:04 +01:00
Steve Waterworth
247fbb02df Add VOLUME to fix fs issue with overlay 2020-08-26 13:08:33 +01:00
Steve Waterworth
7c3ffda0cb change package name 2020-08-26 12:49:20 +01:00
Steve Waterworth
f0043a520b different frameworks 2020-08-26 12:48:37 +01:00
Steve Waterworth
caf9f7b9dd first failed attempt 2020-08-25 17:39:53 +01:00
Cedric Ziel
c47eb4a1db Merge branch 'master' into synthetics 2020-08-25 09:17:26 +02:00
Cedric Ziel
f67e3f6f87 Merge pull request #39 from instana/cedricziel-patch-1
Add bare-bones github actions workflow for building robot-shop
2020-08-25 09:17:09 +02:00
Cedric Ziel
090851e57a Merge branch 'master' into synthetics 2020-08-25 08:59:16 +02:00
Cedric Ziel
c043fd38af Create push.yml 2020-08-24 16:29:09 +02:00
Steve Waterworth
1d5d632dd8 update mysql versions 2020-07-06 10:29:48 +01:00
Cedric Ziel
7e6b390dd9 Merge branch 'master' into synthetics 2020-06-23 14:55:49 +02:00
Steve Waterworth
1ab2ee68af locust breaking changes 2020-06-23 10:37:11 +01:00
Steve Waterworth
4242f59c3f request latest version 2020-06-23 10:18:29 +01:00
Steve Waterworth
25bcebfc59 bump version 2020-06-23 10:13:49 +01:00
Steve Waterworth
184093b14b fix indent 2020-06-23 10:13:30 +01:00
Cedric Ziel
6af5a3bcf4 Correctly add CORS header in responseFilter 2020-06-22 11:34:07 +02:00
Cedric Ziel
8ea413da09 Wire HealthCheckService in container 2020-06-22 11:27:17 +02:00
Cedric Ziel
09b571c87e Use correct parameter type for LoggingProcessor header crunching 2020-06-22 11:27:16 +02:00
Cedric Ziel
f06a04ff86 Set default pullPolicy of IfNotPresent for helm chart
This will allow preloading images into the cluster
so the images dont have to be pulled. This is useful
for images/tags that are currently not in a public
registry. Otherwise it will try pulling anyways
and fail even if the image is already known to the
local daemon.
2020-06-22 11:27:16 +02:00
Cedric Ziel
4063325ac1 Add healthchecks to shipping service 2020-06-22 11:27:16 +02:00
Cedric Ziel
ac8753e19e Add k8s readinessProbe for ratings service 2020-06-22 11:27:16 +02:00
Cedric Ziel
6bcb2872c3 Introduce docker-compose healtcheck using X-INSTANA-SYNTHETIC 2020-06-22 11:27:16 +02:00
Cedric Ziel
83e1977b2f Transform ratings service to Symfony MicroKernel application
The ratings service contained a PHP 4 style code. This change
introduces a framework for processing the http-requests so we
dont have to do it on our own.

It'll also showcase how Instana is usable with modern PHP.
2020-06-22 11:26:49 +02:00
Cedric Ziel
9369788ced Merge pull request #35 from instana/proooooofiles 2020-06-22 11:12:14 +02:00
Cedric Ziel
f98c59a891 Remove unreachable panic and correctly sprintf 2020-06-22 11:10:02 +02:00
Cedric Ziel
fcb1643bef Use empty select to block, not custom channel 2020-06-22 11:09:33 +02:00
Cedric Ziel
1f6f0bd745 Return early on error in queue setup 2020-06-22 11:07:08 +02:00
Cedric Ziel
2fa53fc11e Enable AutoProfiling for golang component 2020-06-22 10:52:21 +02:00
Cedric Ziel
cf0ada6d51 Merge pull request #34 from mmanciop/patch-3 2020-06-22 09:49:42 +02:00
Cedric Ziel
a2e783d333 Merge pull request #33 from mmanciop/patch-2 2020-06-22 09:49:27 +02:00
Cedric Ziel
136dd5f987 Merge pull request #32 from instana/node-profiling 2020-06-22 09:48:53 +02:00
Michele Mancioppi
c2b9581795 Fix mispelling 2020-06-08 13:41:31 +02:00
Michele Mancioppi
1f1c7719c1 Fix mispelling 2020-06-08 13:40:43 +02:00
Cedric Ziel
15b13c69f9 Upgrade to instana/collector 1.98.1 to accommodate fix for containers 2020-05-25 09:59:20 +02:00
Cedric Ziel
4b201e58c3 Utilize Instana AutoProfile for NodeJS
* update NodeJS collector to a minimum of v1.98.0
* set INSTANA_AUTO_PROFILE=true so profiling is automatically enabled
2020-05-25 09:59:12 +02:00
Ben Blackmore
661111b932 Use new JS snippet for website monitoring 2020-05-16 12:24:10 +02:00
Steve Waterworth
f7e9349742 more openshift notes 2020-05-05 12:31:08 +01:00
Steve Waterworth
c7b0f2a98d updates for OpenShift 2020-05-05 12:22:25 +01:00
Steve Waterworth
9ecba3f164 new utility 2020-04-30 12:29:15 +01:00
Steve Waterworth
51f5ea6ecf remove test pv 2020-04-22 16:32:01 +01:00
Steve Waterworth
50d4ab5406 remove test log message 2020-04-22 11:24:01 +01:00
Steve Waterworth
37c4a2a9c3 redis as a statefulset 2020-04-21 15:13:47 +01:00
126 changed files with 2399 additions and 1113 deletions

2
.env
View File

@@ -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
View 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
View File

@@ -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

View File

@@ -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` |

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -20,6 +20,7 @@ spec:
containers:
- name: rabbitmq
image: rabbitmq:3.7-management-alpine
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 5672
- containerPort: 15672

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}}

View File

@@ -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

View File

@@ -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
```

View File

@@ -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 " "

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,6 @@
FROM node:10
FROM node:14
ENV INSTANA_AUTO_PROFILE true
EXPOSE 8080

View File

@@ -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"
}
}

View File

@@ -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());

View File

@@ -1,4 +1,6 @@
FROM node:10
FROM node:14
ENV INSTANA_AUTO_PROFILE true
EXPOSE 8080

View File

@@ -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"
}
}

View File

@@ -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());

View File

@@ -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
View 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 {}
}

View File

@@ -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
}

View File

@@ -10,3 +10,8 @@ services:
- robot-shop
depends_on:
- web
logging: &logging
driver: "json-file"
options:
max-size: "25m"
max-file: "2"

View File

@@ -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:

View 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.

View 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
View 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
View 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

View 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.

View 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
View 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
View 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

View File

@@ -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"]

View File

@@ -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.

View File

@@ -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

View File

@@ -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
View File

View File

@@ -1 +1 @@
locustio
locust

View File

@@ -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

View 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)

View 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

View File

@@ -1,4 +1,4 @@
FROM mongo:3.6.1
FROM mongo:5
COPY *.js /docker-entrypoint-initdb.d/

View File

@@ -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

View File

@@ -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/*

View File

@@ -1,4 +1,4 @@
FROM python:3.6
FROM python:3.9
EXPOSE 8080
USER root

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -1,5 +0,0 @@
{
"require": {
"monolog/monolog": "^1.24.0"
}
}

View File

@@ -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>

View File

@@ -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']);
}
}
?>

View File

@@ -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()));
}
?>

View 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
View 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);

View File

@@ -1 +1,3 @@
<?php phpinfo(); ?>
<?php
phpinfo();

View 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);
}
}

View 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));
}
}

View 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;
}
}
}

View 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());
}
}
}

View File

@@ -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
View 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');
}
}

View 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;
}
}

View 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();
}
}

View 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
View File

View File

4
shipping/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/target
/.classpath
/.project
/.settings

View File

@@ -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" ]

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View 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

View File

@@ -1,4 +1,6 @@
FROM node:10
FROM node:14
ENV INSTANA_AUTO_PROFILE true
EXPOSE 8080

View File

@@ -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"
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;"

View File

@@ -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

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