├── web-shop ├── .dockerignore ├── static │ ├── img │ │ ├── banner.png │ │ ├── cats │ │ │ ├── Carla.jpeg │ │ │ ├── Loki.jpeg │ │ │ ├── Meows.jpeg │ │ │ └── Charlie.jpeg │ │ ├── energy │ │ │ ├── modem.png │ │ │ ├── wallbox.png │ │ │ ├── heatpump.png │ │ │ └── solarpanel.png │ │ ├── food │ │ │ ├── chicken.png │ │ │ ├── pasta.png │ │ │ ├── salmon.png │ │ │ └── shrimps.png │ │ └── phones │ │ │ ├── OPPO.jpeg │ │ │ ├── ZTE.jpeg │ │ │ ├── XIAOMI.jpeg │ │ │ └── SAMSUNG.jpeg │ ├── Grafana_LinkedIn_banner4.png │ ├── main.js.LICENSE.txt │ └── main.js ├── build.sh ├── wsgi.ini ├── web_shop.py ├── config.py ├── Dockerfile ├── requirements.txt ├── templates │ ├── login.html │ ├── index.html │ ├── cart.html │ └── layout.html ├── wsgi.py └── routes.py ├── products ├── settings.gradle ├── httpserver.yml ├── run.sh ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.sh ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── datahovel │ │ │ └── webshop │ │ │ ├── repository │ │ │ └── ProductRepository.java │ │ │ ├── kafka │ │ │ └── Producer.java │ │ │ ├── PprofController.java │ │ │ ├── model │ │ │ └── Product.java │ │ │ ├── ProductController.java │ │ │ └── Application.java │ │ └── resources │ │ └── application.properties ├── Dockerfile ├── build.gradle ├── pom.xml ├── gradlew.bat ├── gradlew ├── mvnw.cmd └── mvnw ├── shopping-cart ├── mariadb │ └── init.sql ├── build.sh ├── wsgi.ini ├── Dockerfile ├── cart.py ├── requirements.txt ├── config.py ├── wsgi.py ├── models.py └── routes.py ├── images ├── web-shop-ui.png ├── web-shop-traces.png ├── web-shop-dashboard.png └── web-shop-architecture.png ├── xk6-browser ├── Readme.md └── Dockerfile ├── kubernetes ├── 01-namespace.yaml ├── web-shop.yaml ├── deploy.sh ├── shopping-cart.yaml ├── products.yaml └── mariadb.yaml ├── .gitignore ├── grafana ├── conf │ └── provisioning │ │ ├── dashboards │ │ └── dashboards.yaml │ │ └── datasources │ │ └── datasources.yaml └── dashboards │ └── business │ └── Customer_View.json ├── blackbox_exporter └── blackbox.yml ├── simulate-user-traffic.sh ├── simulate-browser-traffic.sh ├── up.sh ├── mimir └── demo.yaml ├── tempo └── tempo.yaml ├── squid └── squid.conf ├── shop-simulator ├── webshop-browser-demo.js ├── webshop-browser-and-protocol-demo.js ├── webshop-protocol-demo-food.js ├── webshop-protocol-demo.js └── webshop-protocol-demo-phones.js ├── agent └── config.yaml ├── setup └── add_products.sh ├── README.md ├── complete-demo ├── docker-compose.yml └── docker-compose.yml.bakcup ├── docker-compose.yml └── slos └── web-shop-SLO-rules.yml /web-shop/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /products/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-boot' 2 | -------------------------------------------------------------------------------- /products/httpserver.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rules: 3 | - pattern: ".*" 4 | 5 | -------------------------------------------------------------------------------- /products/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec "java" "${JAVA_OPTS}" "-jar app.jar" 3 | -------------------------------------------------------------------------------- /shopping-cart/mariadb/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS webshopdb; 2 | -------------------------------------------------------------------------------- /images/web-shop-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/images/web-shop-ui.png -------------------------------------------------------------------------------- /images/web-shop-traces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/images/web-shop-traces.png -------------------------------------------------------------------------------- /images/web-shop-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/images/web-shop-dashboard.png -------------------------------------------------------------------------------- /web-shop/static/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/banner.png -------------------------------------------------------------------------------- /images/web-shop-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/images/web-shop-architecture.png -------------------------------------------------------------------------------- /xk6-browser/Readme.md: -------------------------------------------------------------------------------- 1 | # Docker image build for k6 with xk6-browser 2 | 3 | ``` 4 | docker build -t xk6-browser:v0.7.0 . 5 | ``` -------------------------------------------------------------------------------- /web-shop/static/img/cats/Carla.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/cats/Carla.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/cats/Loki.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/cats/Loki.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/cats/Meows.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/cats/Meows.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/energy/modem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/energy/modem.png -------------------------------------------------------------------------------- /web-shop/static/img/food/chicken.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/food/chicken.png -------------------------------------------------------------------------------- /web-shop/static/img/food/pasta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/food/pasta.png -------------------------------------------------------------------------------- /web-shop/static/img/food/salmon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/food/salmon.png -------------------------------------------------------------------------------- /web-shop/static/img/food/shrimps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/food/shrimps.png -------------------------------------------------------------------------------- /web-shop/static/img/phones/OPPO.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/phones/OPPO.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/phones/ZTE.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/phones/ZTE.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/cats/Charlie.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/cats/Charlie.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/energy/wallbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/energy/wallbox.png -------------------------------------------------------------------------------- /web-shop/static/img/phones/XIAOMI.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/phones/XIAOMI.jpeg -------------------------------------------------------------------------------- /web-shop/static/img/energy/heatpump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/energy/heatpump.png -------------------------------------------------------------------------------- /web-shop/static/img/energy/solarpanel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/energy/solarpanel.png -------------------------------------------------------------------------------- /web-shop/static/img/phones/SAMSUNG.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/img/phones/SAMSUNG.jpeg -------------------------------------------------------------------------------- /products/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/products/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kubernetes/01-namespace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Namespace 3 | apiVersion: v1 4 | metadata: 5 | name: web-shop-app 6 | labels: 7 | name: web-shop-app -------------------------------------------------------------------------------- /web-shop/static/Grafana_LinkedIn_banner4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Condla/web-shop-o11y-demo/HEAD/web-shop/static/Grafana_LinkedIn_banner4.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | __pycache__ 3 | target 4 | .mvn 5 | opentelemetry-javaagent.jar 6 | jmx_prometheus_javaagent-0.17.2.jar 7 | .vscode 8 | kubernetes/set_env.sh 9 | setup/*.log 10 | *.log 11 | node_modules 12 | -------------------------------------------------------------------------------- /web-shop/build.sh: -------------------------------------------------------------------------------- 1 | IMAGE_NAME="condla/web-shop" 2 | VERSION="2.3" 3 | docker build . -t $IMAGE_NAME:$VERSION 4 | docker build . -t $IMAGE_NAME:latest 5 | docker push $IMAGE_NAME:$VERSION 6 | docker push $IMAGE_NAME:latest 7 | -------------------------------------------------------------------------------- /products/build.sh: -------------------------------------------------------------------------------- 1 | IMAGE_NAME="condla/products" 2 | VERSION="otel-1.9" 3 | docker build . -t $IMAGE_NAME:$VERSION 4 | docker build . -t $IMAGE_NAME:latest 5 | docker push $IMAGE_NAME:$VERSION 6 | docker push $IMAGE_NAME:latest 7 | -------------------------------------------------------------------------------- /shopping-cart/build.sh: -------------------------------------------------------------------------------- 1 | IMAGE_NAME=condla/shopping-cart 2 | VERSION=1.5 3 | docker build . -t $IMAGE_NAME:$VERSION 4 | docker build . -t $IMAGE_NAME:latest 5 | docker push $IMAGE_NAME:$VERSION 6 | docker push $IMAGE_NAME:latest 7 | 8 | -------------------------------------------------------------------------------- /web-shop/wsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = :3389 3 | wsgi-file = wsgi.py 4 | callable = app 5 | processes = 1 6 | threads = 2 7 | master = true 8 | enable-threads = true 9 | chmod-socket = 664 10 | vacuum = true 11 | die-on-term = true 12 | -------------------------------------------------------------------------------- /shopping-cart/wsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = :5555 3 | wsgi-file = wsgi.py 4 | callable = app 5 | processes = 1 6 | threads = 2 7 | master = true 8 | enable-threads = true 9 | chmod-socket = 664 10 | vacuum = true 11 | die-on-term = true 12 | -------------------------------------------------------------------------------- /products/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /grafana/conf/provisioning/dashboards/dashboards.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Holistic Webshop Observability' 5 | orgId: 1 6 | type: file 7 | disableDeletion: false 8 | editable: false 9 | updateIntervalSeconds: 10 10 | options: 11 | path: /var/lib/grafana/dashboards/ 12 | -------------------------------------------------------------------------------- /web-shop/web_shop.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | def create_app(): 4 | """Construct the core application.""" 5 | app = Flask(__name__, instance_relative_config=False) 6 | app.config.from_object('config.Config') 7 | 8 | with app.app_context(): 9 | import routes # Import routes 10 | return app -------------------------------------------------------------------------------- /web-shop/config.py: -------------------------------------------------------------------------------- 1 | """Flask configuration variables.""" 2 | from os import environ 3 | 4 | 5 | class Config: 6 | """Set Flask configuration from .env file.""" 7 | 8 | # General Config 9 | SECRET_KEY = environ.get('SECRET_KEY', "secretkey") 10 | FLASK_APP = environ.get('FLASK_APP', ) 11 | FLASK_ENV = environ.get('FLASK_ENV') -------------------------------------------------------------------------------- /blackbox_exporter/blackbox.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | http_2xx: 3 | http: 4 | fail_if_not_ssl: false 5 | ip_protocol_fallback: false 6 | method: GET 7 | no_follow_redirects: false 8 | preferred_ip_protocol: ip4 9 | valid_http_versions: 10 | - HTTP/1.1 11 | - HTTP/2.0 12 | prober: http 13 | timeout: 15s -------------------------------------------------------------------------------- /products/src/main/java/com/datahovel/webshop/repository/ProductRepository.java: -------------------------------------------------------------------------------- 1 | package com.datahovel.webshop.repository; 2 | 3 | import com.datahovel.webshop.model.Product; 4 | 5 | import org.springframework.data.repository.CrudRepository; 6 | 7 | public interface ProductRepository extends CrudRepository { 8 | Product findByName(String name); 9 | } 10 | -------------------------------------------------------------------------------- /web-shop/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | LABEL Stefan List 3 | WORKDIR /app 4 | RUN apt-get update -y && apt-get install -y python3-pip python3-dev build-essential libmariadb-dev && rm -rf /var/lib/apt/lists/* 5 | COPY ./requirements.txt /app/requirements.txt 6 | RUN pip3 install -r /app/requirements.txt 7 | COPY . /app 8 | WORKDIR /app 9 | CMD [ "uwsgi", "--ini", "wsgi.ini" ] 10 | -------------------------------------------------------------------------------- /shopping-cart/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | LABEL Stefan List 3 | WORKDIR /app 4 | RUN apt-get update -y && apt-get install -y python3-pip python3-dev build-essential libmariadb-dev && rm -rf /var/lib/apt/lists/* 5 | COPY ./requirements.txt /app/requirements.txt 6 | RUN pip3 install -r /app/requirements.txt 7 | COPY . /app 8 | WORKDIR /app 9 | CMD [ "uwsgi", "--ini", "wsgi.ini" ] 10 | -------------------------------------------------------------------------------- /web-shop/static/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** 10 | * @license Angular v12.0.0-next.0 11 | * (c) 2010-2020 Google LLC. https://angular.io/ 12 | * License: MIT 13 | */ 14 | -------------------------------------------------------------------------------- /xk6-browser/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-bullseye as builder 2 | 3 | RUN go install -trimpath go.k6.io/xk6/cmd/xk6@latest 4 | 5 | RUN xk6 build --output "/tmp/k6" --with github.com/grafana/xk6-browser 6 | 7 | FROM debian:bullseye 8 | 9 | RUN apt-get update && \ 10 | apt-get install -y chromium 11 | 12 | COPY --from=builder /tmp/k6 /usr/bin/k6 13 | 14 | ENV XK6_HEADLESS=true 15 | 16 | ENTRYPOINT ["k6"] -------------------------------------------------------------------------------- /shopping-cart/cart.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | db = SQLAlchemy() 5 | 6 | def create_app(): 7 | """Construct the core application.""" 8 | app = Flask(__name__, instance_relative_config=False) 9 | app.config.from_object('config.Config') 10 | db.init_app(app) 11 | 12 | with app.app_context(): 13 | import routes # Import routes 14 | db.create_all() # Create sql tables for our data models 15 | return app 16 | 17 | 18 | -------------------------------------------------------------------------------- /shopping-cart/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.2.0 2 | Werkzeug==2.2.2 3 | requests==2.25.1 4 | protobuf==3.20.1 5 | prometheus-flask-exporter==0.22.3 6 | opentelemetry-api==1.11.1 7 | opentelemetry-sdk==1.11.1 8 | opentelemetry-exporter-otlp==1.11.1 9 | opentelemetry-instrumentation-flask==0.30b1 10 | opentelemetry-instrumentation-requests==0.30b1 11 | opentelemetry-instrumentation-logging==0.30b1 12 | opentelemetry-instrumentation-wsgi==0.30b1 13 | opentelemetry-instrumentation-sqlalchemy==0.30b1 14 | uwsgi==2.0.20 15 | mariadb==1.0.9 16 | flask_sqlalchemy==3.0.2 -------------------------------------------------------------------------------- /web-shop/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.2.0 2 | Werkzeug==2.2.2 3 | requests==2.25.1 4 | protobuf==3.20.1 5 | prometheus-flask-exporter==0.22.3 6 | opentelemetry-api==1.11.1 7 | opentelemetry-sdk==1.11.1 8 | opentelemetry-exporter-otlp==1.11.1 9 | opentelemetry-instrumentation-flask==0.30b1 10 | opentelemetry-instrumentation-requests==0.30b1 11 | opentelemetry-instrumentation-logging==0.30b1 12 | opentelemetry-instrumentation-wsgi==0.30b1 13 | opentelemetry-instrumentation-sqlalchemy==0.30b1 14 | uwsgi==2.0.20 15 | mariadb==1.0.9 16 | flask_sqlalchemy==3.0.2 17 | -------------------------------------------------------------------------------- /shopping-cart/config.py: -------------------------------------------------------------------------------- 1 | """Flask configuration variables.""" 2 | from os import environ 3 | 4 | 5 | class Config: 6 | """Set Flask configuration from .env file.""" 7 | 8 | # General Config 9 | SECRET_KEY = environ.get('SECRET_KEY', "secretkey") 10 | FLASK_APP = environ.get('FLASK_APP', ) 11 | FLASK_ENV = environ.get('FLASK_ENV') 12 | 13 | # Database 14 | SQLALCHEMY_DATABASE_URI = environ.get("SQLALCHEMY_DATABASE_URI", 'mariadb+mariadbconnector://root:myrootpassword@mariadb:3306/webshopdb') 15 | SQLALCHEMY_ECHO = False 16 | SQLALCHEMY_TRACK_MODIFICATIONS = False 17 | -------------------------------------------------------------------------------- /products/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Build stage 3 | # 4 | FROM maven:3.6.0-jdk-11-slim AS build 5 | COPY src /home/app/src 6 | COPY pom.xml /home/app 7 | RUN mvn -f /home/app/pom.xml clean package 8 | # 9 | # Package stage 10 | # 11 | FROM openjdk:11-jre-slim 12 | COPY --from=build /home/app/target/*.jar app.jar 13 | # last tested working build: opentelemetry-javaagent-1.32.0.jar 14 | COPY opentelemetry-javaagent.jar /opentelemetry-javaagent.jar 15 | COPY jmx_prometheus_javaagent-0.17.2.jar /jmx_prometheus_javaagent.jar 16 | COPY httpserver.yml /httpserver.yml 17 | #COPY run.sh /run.sh 18 | #RUN chmod +x /run.sh 19 | EXPOSE 8080 20 | ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar 21 | 22 | -------------------------------------------------------------------------------- /simulate-user-traffic.sh: -------------------------------------------------------------------------------- 1 | echo $PUBLIC_APP_URL 2 | #docker run --network web-shop-o11y-demo_web-shop --rm -i grafana/k6 run -e HOSTNAME=${PUBLIC_APP_URL:=web-shop} - kafkaTemplate; 16 | 17 | public void sendMessage(String message) { 18 | logger.info(String.format("Checked out shopping cart: ", message)); 19 | this.kafkaTemplate.send(TOPIC, message); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /up.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # insert your public IP or URL here 4 | export PUBLIC_APP_URL=${PUBLIC_APP_URL:=grafana.datahovel.com} 5 | #export PUBLIC_APP_URL=${PUBLIC_APP_URL:=localhost} 6 | 7 | docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions && sudo systemctl restart docker 8 | docker plugin ls 9 | 10 | OTEL_JAR="products/opentelemetry-javaagent.jar" 11 | test -f "$OTEL_JAR" || wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v1.23.0/opentelemetry-javaagent.jar -O $OTEL_JAR 12 | 13 | JMX_JAR="products/jmx_prometheus_javaagent-0.17.2.jar" 14 | test -f "$JMX_JAR" || wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.17.2/jmx_prometheus_javaagent-0.17.2.jar -O $JMX_JAR 15 | 16 | if [ "$1" == "build" ] 17 | then 18 | docker-compose up -d --build 19 | else 20 | docker-compose up -d 21 | fi 22 | -------------------------------------------------------------------------------- /mimir/demo.yaml: -------------------------------------------------------------------------------- 1 | # Do not use this configuration in production. 2 | # It is for demonstration purposes only. 3 | multitenancy_enabled: false 4 | 5 | blocks_storage: 6 | backend: filesystem 7 | bucket_store: 8 | sync_dir: /tmp/mimir/tsdb-sync 9 | filesystem: 10 | dir: /tmp/mimir/data/tsdb 11 | tsdb: 12 | dir: /tmp/mimir/tsdb 13 | 14 | compactor: 15 | data_dir: /tmp/mimir/compactor 16 | sharding_ring: 17 | kvstore: 18 | store: memberlist 19 | 20 | distributor: 21 | ring: 22 | instance_addr: 127.0.0.1 23 | kvstore: 24 | store: memberlist 25 | 26 | ingester: 27 | ring: 28 | instance_addr: 127.0.0.1 29 | kvstore: 30 | store: memberlist 31 | replication_factor: 1 32 | 33 | ruler_storage: 34 | backend: filesystem 35 | filesystem: 36 | dir: /tmp/mimir/rules 37 | 38 | server: 39 | http_listen_port: 9009 40 | log_level: error 41 | 42 | store_gateway: 43 | sharding_ring: 44 | replication_factor: 1 -------------------------------------------------------------------------------- /products/src/main/java/com/datahovel/webshop/PprofController.java: -------------------------------------------------------------------------------- 1 | package com.datahovel.webshop; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | import org.springframework.web.bind.annotation.RequestParam; 6 | import org.springframework.web.bind.annotation.ResponseBody; 7 | 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.io.IOException; 10 | import java.time.Duration; 11 | 12 | import jpprof.CPUProfiler; 13 | 14 | @RestController 15 | public class PprofController { 16 | 17 | @GetMapping("/debug/pprof/profile") 18 | @ResponseBody 19 | public void profile(@RequestParam(required = false) String seconds, HttpServletResponse response) { 20 | try { 21 | Duration d = Duration.ofSeconds(Integer.parseInt(seconds)); 22 | CPUProfiler.start(d, response.getOutputStream()); 23 | response.flushBuffer(); 24 | } catch (Exception e) { 25 | System.out.println("exception: " + e.getMessage()); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /web-shop/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 |

Welcome!

5 |

Welcome!

6 |

7 | {% endblock %} 8 | 9 | {% block content %} 10 | 11 | 12 |
13 |

Please Choose Name

14 |
15 | Choose a name and try to:
16 |
    17 |
  • Put a product into the shopping cart.
  • 18 |
  • Then check out the product.
  • 19 |
  • There might be issues and errors along the way, but try as long as needed to successfully check out your item.
  • 20 |

    21 |
    22 | 24 | 25 |
    26 | {% if error %} 27 |

    Error: {{ error }} 28 | {% endif %} 29 |

30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /products/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #Metrics related configurations 2 | management.endpoint.metrics.enabled=true 3 | management.endpoints.web.exposure.include=* 4 | management.endpoint.prometheus.enabled=true 5 | management.metrics.export.prometheus.enabled=true 6 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 7 | logging.pattern.console='%d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg traceID=%X{trace_id} %n' 8 | spring.datasource.url=jdbc:mariadb://mariadb:3306/webshop 9 | spring.datasource.username=root 10 | spring.datasource.password=myrootpassword 11 | spring.datasource.driver-class-name=org.mariadb.jdbc.Driver 12 | spring.jpa.hibernate.ddl-auto=validate 13 | spring.kafka.producer.bootstrap-servers=broker:9092 14 | spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer 15 | spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer 16 | spring.kafka.consumer.bootstrap-servers=broker:9092 17 | spring.kafka.consumer.auto-offset-reset=earliest 18 | spring.kafka.consumer.key-serializer=org.apache.kafka.common.serialization.StringSerializer 19 | spring.kafka.consumer.value-serializer=org.apache.kafka.common.serialization.StringSerializer 20 | kafka.bootstrap=broker:9092 21 | -------------------------------------------------------------------------------- /web-shop/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block title %} 4 |

Welcome {{person}}

5 |

Welcome {{person}}

6 |

7 | {% endblock %} 8 | 9 | 10 | {% block content %} 11 |
12 |

{{ products|length }} items

13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | {% for product in products %} 21 |
22 |
23 |
24 | 25 | {{product["tag"]}} 26 |
27 | 28 | 29 |
30 |
31 |

{{ product.name }}
{{product.price}}

32 |
33 |
34 | {% endfor %} 35 |
36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /tempo/tempo.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 3200 3 | log_level: debug 4 | 5 | distributor: 6 | receivers: 7 | jaeger: 8 | protocols: 9 | thrift_http: 10 | grpc: 11 | thrift_binary: 12 | thrift_compact: 13 | otlp: 14 | protocols: 15 | http: 16 | grpc: 17 | 18 | ingester: 19 | trace_idle_period: 10s 20 | max_block_bytes: 1_000_000 21 | max_block_duration: 5m 22 | 23 | compactor: 24 | compaction: 25 | compaction_window: 1h 26 | max_block_bytes: 100_000_000 27 | block_retention: 1h 28 | compacted_block_retention: 10m 29 | 30 | storage: 31 | trace: 32 | backend: local 33 | wal: 34 | path: /tmp/tempo/wal 35 | local: 36 | path: /tmp/tempo/blocks 37 | pool: 38 | max_workers: 100 39 | queue_depth: 10000 40 | 41 | metrics_generator: 42 | ring: 43 | kvstore: 44 | store: memberlist 45 | processor: 46 | service_graphs: 47 | wait: 10s 48 | max_items: 10000 49 | workers: 10 50 | dimensions: 51 | - team.name 52 | span_metrics: 53 | dimensions: 54 | - team.name 55 | registry: 56 | stale_duration: 5m 57 | 58 | storage: 59 | path: /tmp/data 60 | wal: 61 | truncate_frequency: 0h5m0s 62 | max_wal_time: 900 63 | remote_write: 64 | - url: http://mimir:9009/api/v1/push 65 | 66 | 67 | overrides: 68 | metrics_generator_processors: 69 | - span-metrics 70 | - service-graphs 71 | max_traces_per_user: 100000 72 | max_search_bytes_per_trace: 50000 73 | 74 | multitenancy_enabled: false 75 | 76 | -------------------------------------------------------------------------------- /products/src/main/java/com/datahovel/webshop/model/Product.java: -------------------------------------------------------------------------------- 1 | package com.datahovel.webshop.model; 2 | 3 | import javax.persistence.Entity; 4 | import javax.persistence.GeneratedValue; 5 | import javax.persistence.GenerationType; 6 | import javax.persistence.Id; 7 | import javax.persistence.Table; 8 | 9 | @Entity 10 | @Table(name = "product") 11 | public class Product { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.IDENTITY) 14 | private Integer id; 15 | private String name; 16 | private Float price; 17 | private String tag; 18 | private String pic_ref; 19 | public Product() { 20 | } 21 | public Product(String name, Float price, String tag, String pic_ref) { 22 | this.name = name; 23 | this.price = price; 24 | this.tag = tag; 25 | this.pic_ref = pic_ref; 26 | } 27 | public Integer getId() { 28 | return id; 29 | } 30 | public void setId(Integer id) { 31 | this.id = id; 32 | } 33 | public String getName() { 34 | return name; 35 | } 36 | public void setName(String name) { 37 | this.name = name; 38 | } 39 | public Float getPrice() { 40 | return price; 41 | } 42 | public void setPrice(Float price) { 43 | this.price = price; 44 | } 45 | public String getTag() { 46 | return tag; 47 | } 48 | public void setTag(String tag) { 49 | this.tag = tag; 50 | } 51 | public String getPic_ref() { 52 | return pic_ref; 53 | } 54 | public void setPic_ref(String pic_ref) { 55 | this.pic_ref = pic_ref; 56 | } 57 | 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /web-shop/templates/cart.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} 3 |

{{person}}'s Shopping Cart

4 |

{{person}}'s Shopping Cart

5 |

6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |

11 | 12 | 13 | Product 14 | 15 | 16 | 17 | 18 | {% set i=0 %} 19 | {% for item in items %} 20 | {% set i = i + 1 %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 |
QuantityUnit PriceTotal Price
{{ item.product.name }} {{ item.quantity}} {{ '%0.2f' | format(item.product.price) | float }} {{ '%0.2f' | format(item.quantity * item.product.price) | float }}
29 |

30 | 33 |

34 |

35 | 38 |

39 |

40 | 43 |

44 |
45 | 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /kubernetes/web-shop.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ##################################### 3 | ############# web-shop ############# 4 | ##################################### 5 | apiVersion: v1 6 | kind: Service 7 | metadata: 8 | namespace: web-shop-app 9 | labels: 10 | app: web-shop 11 | name: web-shop 12 | spec: 13 | ports: 14 | - name: "core" 15 | port: 3389 16 | targetPort: 3389 17 | type: LoadBalancer 18 | selector: 19 | app: web-shop 20 | --- 21 | apiVersion: apps/v1 22 | kind: Deployment 23 | metadata: 24 | namespace: web-shop-app 25 | labels: 26 | app: web-shop 27 | name: web-shop 28 | spec: 29 | replicas: 1 30 | selector: 31 | matchLabels: 32 | app: web-shop 33 | template: 34 | metadata: 35 | labels: 36 | app: web-shop 37 | spec: 38 | containers: 39 | - name: web-shop 40 | image: condla/web-shop:2.3 41 | env: 42 | - name: OTEL_EXPORTER_OTLP_ENDPOINT 43 | value: grafana-k8s-monitoring-grafana-agent.agent.svc.cluster.local:4317 44 | - name: APP_AGENT_RECEIVER_ENDPOINT 45 | value: $BROWSER_ENDPOINT 46 | command: 47 | ports: 48 | - containerPort: 3389 49 | volumeMounts: 50 | livenessProbe: 51 | httpGet: 52 | path: /shop 53 | port: 3389 54 | initialDelaySeconds: 10 55 | periodSeconds: 10 56 | timeoutSeconds: 5 57 | failureThreshold: 6 58 | successThreshold: 1 59 | readinessProbe: 60 | httpGet: 61 | path: /shop 62 | port: 3389 63 | initialDelaySeconds: 5 64 | periodSeconds: 10 65 | timeoutSeconds: 5 66 | failureThreshold: 6 67 | successThreshold: 1 68 | volumes: 69 | --- 70 | -------------------------------------------------------------------------------- /kubernetes/deploy.sh: -------------------------------------------------------------------------------- 1 | export MIMIR_URL=https://prometheus-prod-10-prod-us-central-0.grafana.net 2 | export MIMIR_USR=364318 3 | export MIMIR_KEY=YOURKEY 4 | 5 | export LOKI_URL=https://logs-prod3.grafana.net 6 | export LOKI_USR=181129 7 | export LOKI_KEY=$MIMIR_KEY 8 | 9 | export TEMPO_URL=https://tempo-us-central1.grafana.net:443 10 | export TEMPO_USR=177642 11 | export TEMPO_KEY=$MIMIR_KEY 12 | 13 | export BROWSER_ENDPOINT=https://faro-collector-prod-us-central-0.grafana.net/collect/fd7d8aadd5870ecae33bbfc1ee28e3e7 14 | 15 | # you need kubectl and helm installed on your machine 16 | 17 | 18 | kubectl apply -f kubernetes/01-namespace.yaml 19 | 20 | kubectl apply -f kubernetes/mariadb.yaml 21 | kubectl apply -f kubernetes/kafka-cluster.yaml 22 | kubectl apply -f kubernetes/shopping-cart.yaml 23 | kubectl apply -f kubernetes/products.yaml 24 | envsubst < kubernetes/web-shop.yaml | kubectl apply -f - 25 | 26 | #envsubst < kubernetes/04-xk6-browser.yaml | kubectl apply -f - 27 | 28 | 29 | 30 | helm repo add grafana https://grafana.github.io/helm-charts && 31 | helm repo update && 32 | helm upgrade --install --atomic --timeout 120s grafana-k8s-monitoring grafana/k8s-monitoring \ 33 | --namespace "agent" --create-namespace --values - < products; 29 | 30 | private final Producer producer; 31 | ObjectMapper objectMapper = new ObjectMapper(); 32 | 33 | @Autowired 34 | ProductController(Producer producer) { 35 | this.producer = producer; 36 | } 37 | 38 | 39 | @GetMapping("/") 40 | public Iterable getAllProducts() { 41 | 42 | products = productRepository.findAll(); 43 | return products; 44 | } 45 | 46 | @PostMapping("/") 47 | public Product addProduct(@RequestBody Product product) throws ServerException { 48 | product = productRepository.save(product); 49 | if (product == null) { 50 | logger.error("Couldn't add product to database."); 51 | throw new ServerException("null"); 52 | } 53 | else { 54 | return product; 55 | } 56 | } 57 | 58 | @PostMapping("/checkout") 59 | public void sendToKafka(@RequestBody String message) throws JsonProcessingException { 60 | logger.info(message); 61 | this.producer.sendMessage(message); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /shop-simulator/webshop-browser-demo.js: -------------------------------------------------------------------------------- 1 | import { chromium } from 'k6/experimental/browser'; 2 | import exec from 'k6/execution'; 3 | import { sleep } from 'k6'; 4 | import { check } from 'k6'; 5 | 6 | let users= ["Stefan", "Emil", "Alexandra", "Maja"]; 7 | let random = Math.floor(Math.random() * users.length); 8 | 9 | export const options = { 10 | scenarios: { 11 | browser: { 12 | executor: 'constant-vus', 13 | exec: 'browser', 14 | vus: 1, 15 | duration: '1m', 16 | }, 17 | }, 18 | }; 19 | 20 | export async function browser() { 21 | const browser = chromium.launch({ 22 | headless: true, 23 | slowMo: '500ms', 24 | }); 25 | 26 | const context = browser.newContext({ 27 | screen: {width: 1024, height: 768}, 28 | viewport: { width: 1024, height: 768} 29 | }); 30 | const page = context.newPage(); 31 | try { 32 | random = Math.floor(Math.random() * users.length); 33 | let user = users[random]; 34 | await page.goto('http://grafana.datahovel.com:3389/shop?name='+user); 35 | 36 | 37 | let selector, elem; 38 | 39 | page.mouse.click(344, 402); 40 | sleep(1); 41 | page.mouse.click(530, 410); 42 | sleep(1); 43 | 44 | selector='[href="/cart?name='+user+'"]'; 45 | elem = page.$(selector); 46 | elem.click(); 47 | sleep(1); 48 | 49 | selector='.w3-main'; 50 | elem = page.$(selector); 51 | 52 | let re= /Loki\s*(.*)\s*(.*)\s*(.*)\s*Meows\s*(.*)\s*(.*)\s*(.*)\s*/; 53 | let match = elem.textContent().match(re); 54 | if(match== null) { 55 | re= /Meows\s*(.*)\s*(.*)\s*(.*)\s*Loki\s*(.*)\s*(.*)\s*(.*)\s*/; 56 | match = elem.textContent().match(re); 57 | } 58 | 59 | let total= Number(match[3])+Number(match[6]); 60 | console.log("Total: "+total); 61 | 62 | check(page, { in: "69.98"== total}); 63 | 64 | sleep(1); 65 | 66 | // page.screenshot({ path:"kitties.png" }); 67 | 68 | selector="[id='myBtn']"; 69 | elem = page.$(selector); 70 | elem.click(); 71 | sleep(1); 72 | elem.click(); 73 | sleep(1); 74 | 75 | selector='[name="checkout_cart"]'; 76 | elem = page.$(selector); 77 | elem.click(); 78 | 79 | selector="[onclick='do_something_cool()']"; 80 | elem = page.$(selector); 81 | elem.click(); 82 | sleep(1);} 83 | finally { 84 | page.close(); 85 | browser.close(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /kubernetes/products.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ##################################### 3 | ############# products ############## 4 | ##################################### 5 | apiVersion: v1 6 | kind: Service 7 | metadata: 8 | namespace: web-shop-app 9 | labels: 10 | app: products 11 | name: products 12 | spec: 13 | ports: 14 | - name: "core" 15 | port: 8080 16 | targetPort: 8080 17 | selector: 18 | app: products 19 | --- 20 | apiVersion: apps/v1 21 | kind: Deployment 22 | metadata: 23 | namespace: web-shop-app 24 | labels: 25 | app: products 26 | name: products 27 | spec: 28 | replicas: 1 29 | selector: 30 | matchLabels: 31 | app: products 32 | template: 33 | metadata: 34 | labels: 35 | app: products 36 | spec: 37 | containers: 38 | - name: products 39 | image: condla/products:otel-1.9 40 | env: 41 | - name: OTEL_METRICS_EXPORTER 42 | value: logging 43 | - name: OTEL_LOGS_EXPORTER 44 | value: logging 45 | - name: OTEL_EXPORTER_OTLP_ENDPOINT 46 | value: http://grafana-k8s-monitoring-grafana-agent.agent.svc.cluster.local:4317 47 | - name: OTEL_RESOURCE_ATTRIBUTES 48 | value: service.name=products,team.name=backend,environment=production 49 | - name: JAVA_OPTS 50 | value: -javaagent:/opentelemetry-javaagent.jar 51 | - name: SPRING_KAFKA_PRODUCER_BOOTSTRAP_SERVERS 52 | value: broker:9092 53 | - name: SPRING_KAFKA_CONSUMER_BOOTSTRAP_SERVERS 54 | value: broker:9092 55 | - name: SPRING_KAFKA_BOOTSTRAP_SERVERS 56 | value: broker:9092 57 | - name: KAFKA_BOOTSTRAP 58 | value: broker:9092 59 | command: 60 | ports: 61 | - containerPort: 8080 62 | volumeMounts: 63 | livenessProbe: 64 | httpGet: 65 | path: /actuator/health 66 | port: 8080 67 | initialDelaySeconds: 10 68 | periodSeconds: 10 69 | timeoutSeconds: 5 70 | failureThreshold: 6 71 | successThreshold: 1 72 | readinessProbe: 73 | httpGet: 74 | path: /actuator/health 75 | port: 8080 76 | initialDelaySeconds: 5 77 | periodSeconds: 10 78 | timeoutSeconds: 5 79 | failureThreshold: 6 80 | successThreshold: 1 81 | volumes: 82 | 83 | -------------------------------------------------------------------------------- /products/src/main/java/com/datahovel/webshop/Application.java: -------------------------------------------------------------------------------- 1 | package com.datahovel.webshop; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.apache.kafka.clients.admin.AdminClientConfig; 7 | import org.apache.kafka.clients.admin.NewTopic; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.boot.CommandLineRunner; 11 | import org.springframework.boot.SpringApplication; 12 | import org.springframework.boot.autoconfigure.SpringBootApplication; 13 | import org.springframework.context.ApplicationContext; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.kafka.annotation.KafkaListener; 16 | import org.springframework.kafka.config.TopicBuilder; 17 | import org.springframework.kafka.core.KafkaAdmin; 18 | import org.springframework.web.filter.CommonsRequestLoggingFilter; 19 | 20 | @SpringBootApplication 21 | public class Application { 22 | 23 | 24 | @Value("${kafka.bootstrap}") 25 | String bootstrapServer; 26 | 27 | Logger logger = LoggerFactory.getLogger(Application.class); 28 | public static void main(String[] args) { 29 | SpringApplication.run(Application.class, args); 30 | } 31 | 32 | @Bean 33 | public CommandLineRunner commandLineRunner(ApplicationContext ctx) { 34 | return args -> { 35 | 36 | System.out.println("Application started successfully"); 37 | 38 | }; 39 | } 40 | 41 | @Bean 42 | public CommonsRequestLoggingFilter requestLoggingFilter() { 43 | CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter(); 44 | loggingFilter.setIncludeClientInfo(true); 45 | loggingFilter.setIncludeQueryString(true); 46 | loggingFilter.setIncludePayload(true); 47 | loggingFilter.setIncludeHeaders(true); 48 | return loggingFilter; 49 | } 50 | 51 | @Bean 52 | public KafkaAdmin admin() { 53 | Map configs = new HashMap<>(); 54 | //configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 55 | configs.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServer); 56 | return new KafkaAdmin(configs); 57 | } 58 | 59 | @Bean 60 | public NewTopic products() { 61 | return TopicBuilder.name("products") 62 | .partitions(1) 63 | .replicas(1) 64 | .compact() 65 | .build(); 66 | } 67 | 68 | @KafkaListener(groupId = "products-api", topics = "shopping_cart_stream") 69 | public void consume(String message) { 70 | logger.info(message); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /shopping-cart/wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from cart import create_app, db 4 | import logging 5 | from os import environ 6 | from uwsgidecorators import postfork 7 | from prometheus_flask_exporter import PrometheusMetrics 8 | from opentelemetry import trace 9 | from opentelemetry.sdk.trace import TracerProvider 10 | from opentelemetry.sdk.trace.export import ( 11 | ConsoleSpanExporter, 12 | SimpleSpanProcessor, 13 | ) 14 | from opentelemetry.sdk.trace.export import BatchSpanProcessor 15 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 16 | from opentelemetry.sdk.resources import Resource 17 | from opentelemetry.instrumentation.flask import FlaskInstrumentor 18 | from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor 19 | from opentelemetry.instrumentation.logging import LoggingInstrumentor 20 | from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware 21 | 22 | app = create_app() 23 | 24 | # this postfork thingy is only required, when app runs in a wsgi container. 25 | # this is explained here: https://opentelemetry-python.readthedocs.io/en/latest/examples/fork-process-model/README.html 26 | # need to try with NGINX as an app server 27 | @postfork 28 | def init_tracing(): 29 | resource = Resource(attributes={"service.name": "shopping-cart", "team.name": "backend", "environment":"production"}) 30 | trace.set_tracer_provider(TracerProvider(resource=resource)) 31 | otlp_exporter = OTLPSpanExporter(endpoint=environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"), insecure=True) 32 | span_processor = BatchSpanProcessor(otlp_exporter) 33 | trace.get_tracer_provider().add_span_processor(span_processor) 34 | 35 | # uncomment for local OTel debugging to get traces/span to console: 36 | #trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) 37 | 38 | # be careful not to call this line before the paragraph above! 39 | # getting the tracer explicitly is only required for the LoggingInstrumentor below 40 | # using "opentelemetry-instrument" to do auto instrumentation does not work either for the same reason 41 | tracer = trace.get_tracer(__name__) 42 | 43 | # instrumentation 44 | PrometheusMetrics(app) 45 | FlaskInstrumentor().instrument_app(app) 46 | app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) 47 | with app.app_context(): 48 | SQLAlchemyInstrumentor().instrument(engine=db.engine) 49 | log_format = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] traceID=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s - %(message)s" 50 | LoggingInstrumentor().instrument(set_logging_format=True, logging_format=log_format, log_level=logging.INFO, tracer_provider=tracer) 51 | 52 | 53 | 54 | if __name__ == "__main__": 55 | #if you set debug to True, then the /metrics endpoint returns 404 56 | # this section is only called, when executed directly, not when using 57 | # an app server, such as uwsgi 58 | app.run(debug=False, host='0.0.0.0', port=5555) -------------------------------------------------------------------------------- /web-shop/wsgi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from web_shop import create_app 4 | import logging 5 | from os import environ 6 | from uwsgidecorators import postfork 7 | from prometheus_flask_exporter import PrometheusMetrics 8 | from opentelemetry import trace 9 | from opentelemetry.sdk.trace import TracerProvider 10 | from opentelemetry.sdk.trace.export import ( 11 | ConsoleSpanExporter, 12 | SimpleSpanProcessor, 13 | ) 14 | from opentelemetry.sdk.trace.export import BatchSpanProcessor 15 | from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter 16 | from opentelemetry.sdk.resources import Resource 17 | from opentelemetry.instrumentation.flask import FlaskInstrumentor 18 | from opentelemetry.instrumentation.requests import RequestsInstrumentor 19 | from opentelemetry.instrumentation.logging import LoggingInstrumentor 20 | from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware 21 | 22 | app = create_app() 23 | app.app_agent_receiver_endpoint = environ.get("APP_AGENT_RECEIVER_ENDPOINT") 24 | 25 | # this postfork thingy is only required, when app runs in a wsgi container. 26 | # this is explained here: https://opentelemetry-python.readthedocs.io/en/latest/examples/fork-process-model/README.html 27 | # need to try with NGINX as an app server 28 | @postfork 29 | def init_tracing(): 30 | resource = Resource(attributes={"service.name": "web-shop", "team.name": "frontend", "environment":"production"}) 31 | trace.set_tracer_provider(TracerProvider(resource=resource)) 32 | otlp_exporter = OTLPSpanExporter(endpoint=environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"), insecure=True) 33 | span_processor = BatchSpanProcessor(otlp_exporter) 34 | trace.get_tracer_provider().add_span_processor(span_processor) 35 | 36 | # uncomment for local OTel debugging to get traces/span to console: 37 | #trace.get_tracer_provider().add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) 38 | 39 | # be careful not to call this line before the paragraph above! 40 | # getting the tracer explicitly is only required for the LoggingInstrumentor below 41 | # using "opentelemetry-instrument" to do auto instrumentation does not work either for the same reason 42 | tracer = trace.get_tracer(__name__) 43 | 44 | # instrumentation 45 | PrometheusMetrics(app) 46 | FlaskInstrumentor().instrument_app(app) 47 | app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app) 48 | RequestsInstrumentor().instrument() 49 | log_format = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] traceID=%(otelTraceID)s span_id=%(otelSpanID)s resource.service.name=%(otelServiceName)s - %(message)s" 50 | LoggingInstrumentor().instrument(set_logging_format=True, logging_format=log_format, log_level=logging.INFO, tracer_provider=tracer) 51 | 52 | 53 | 54 | if __name__ == "__main__": 55 | #if you set debug to True, then the /metrics endpoint returns 404 56 | # this section is only called, when executed directly, not when using 57 | # an app server, such as uwsgi 58 | app.run(debug=False, host='0.0.0.0', port=3389) 59 | -------------------------------------------------------------------------------- /products/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.3 9 | 10 | 11 | com.datahovel 12 | products 13 | 0.0.1 14 | products 15 | products service for web shop demo 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | 24 | javax.validation 25 | validation-api 26 | 2.0.1.Final 27 | 28 | 29 | 30 | com.grafana 31 | jpprof 32 | 0.1.4 33 | 34 | 35 | 36 | org.springframework.kafka 37 | spring-kafka 38 | 39 | 40 | 41 | org.mariadb.jdbc 42 | mariadb-java-client 43 | 1.5.7 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-data-jpa 49 | 50 | 51 | 52 | javax.validation 53 | validation-api 54 | 2.0.1.Final 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-actuator 65 | 66 | 67 | 68 | 69 | io.micrometer 70 | micrometer-core 71 | 72 | 73 | 74 | io.micrometer 75 | micrometer-registry-prometheus 76 | 77 | 78 | org.springframework.boot 79 | spring-boot-starter-web 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-starter-actuator 86 | 87 | 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-starter-test 93 | test 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.boot 102 | spring-boot-maven-plugin 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /products/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /shopping-cart/models.py: -------------------------------------------------------------------------------- 1 | from cart import db 2 | from sqlalchemy.orm import relationship, Mapped, mapped_column 3 | from sqlalchemy import func, ForeignKey, String 4 | from dataclasses import dataclass 5 | from flask import jsonify 6 | from datetime import datetime 7 | from typing import Optional, List 8 | 9 | @dataclass 10 | class Customer(db.Model): 11 | __tablename__ = "customer" 12 | id: Mapped[int] = mapped_column(primary_key=True) 13 | name: Mapped[str] = mapped_column(String(50), nullable = False) 14 | created_date: Mapped[datetime] = mapped_column(server_default=func.now()) 15 | 16 | #carts: Mapped[List["Cart"]] = relationship(back_populates='customer') 17 | carts = relationship("Cart", back_populates='customer') 18 | 19 | 20 | @dataclass 21 | class Cart(db.Model): 22 | __tablename__ = "cart" 23 | id: Mapped[int] = mapped_column(primary_key=True) 24 | customer_id: Mapped[int] = mapped_column(ForeignKey('customer.id')) 25 | is_deleted: Mapped[bool] = mapped_column(nullable=False, default=False) 26 | created_date: Mapped[datetime] = mapped_column(server_default=func.now()) 27 | updated_date: Mapped[datetime] = mapped_column(onupdate=func.now(), nullable=True) 28 | 29 | #customer: Mapped["Customer"] = relationship(back_populates="carts") 30 | #cart_items: Mapped[List["CartItem"]] = relationship(back_populates='cart') 31 | #order: Mapped[Optional["Order"]] = relationship(back_populates="cart") 32 | 33 | customer = relationship("Customer", back_populates="carts") 34 | cart_items = relationship("CartItem", back_populates='cart') 35 | order = relationship("Order", back_populates="cart") 36 | 37 | @dataclass 38 | class Product(db.Model): 39 | __tablename__ = "product" 40 | id: Mapped[int] = mapped_column(primary_key=True) 41 | name: Mapped[str] = mapped_column(String(50), nullable = False) 42 | price: Mapped[float] = mapped_column(nullable = False) 43 | tag: Mapped[str] = mapped_column(String(5000), nullable = False) 44 | pic_ref: Mapped[str] = mapped_column(String(5000), nullable = False) 45 | 46 | #cart_items: Mapped[List["CartItem"]] = relationship(back_populates='product') 47 | cart_items = relationship("CartItem", back_populates='product') 48 | 49 | @dataclass 50 | class Order(db.Model): 51 | __tablename__ = "order_table" 52 | 53 | id: Mapped[int] = mapped_column(primary_key=True) 54 | cart_id: Mapped[Optional[int]] = mapped_column(ForeignKey("cart.id")) 55 | order_uuid: Mapped[str] = mapped_column(String(200), nullable=False) 56 | created_date: Mapped[datetime] = mapped_column(server_default=func.now()) 57 | updated_date: Mapped[datetime] = mapped_column(onupdate=func.now(), nullable=True) 58 | 59 | cart: Mapped["Cart"] = relationship(back_populates="order") 60 | #cart = relationship("Cart", back_populates="order") 61 | 62 | @dataclass 63 | class CartItem(db.Model): 64 | __tablename__ = "cart_item" 65 | id: Mapped[int] = mapped_column(primary_key=True) 66 | cart_id: Mapped[Optional[int]] = mapped_column(ForeignKey('cart.id')) 67 | product_id: Mapped[Optional[int]] = mapped_column(ForeignKey('product.id')) 68 | quantity: Mapped[int] = mapped_column(nullable=False) 69 | is_deleted: Mapped[bool] = mapped_column(nullable=False, default=False) 70 | created_date: Mapped[datetime] = mapped_column(server_default=func.now()) 71 | updated_date: Mapped[datetime] = mapped_column(onupdate=func.now(), nullable=True) 72 | 73 | cart: Mapped[Optional["Cart"]] = relationship(back_populates="cart_items") 74 | product: Mapped[Optional["Product"]] = relationship(back_populates="cart_items") 75 | #cart = relationship("Cart", back_populates="cart_items") 76 | #product = relationship("Product", back_populates="cart_items") 77 | 78 | if __name__ == "__main__": 79 | customer = Customer(name='mumi') 80 | cart = Cart(customer=customer) 81 | product = Product(name='product1', price=10.5, tag='special', pic_ref='https://localhost:5555') 82 | cart_item = CartItem(cart=cart, product=product, quantity=1) 83 | print(customer.id) 84 | print(customer.name) 85 | print(cart.id) 86 | print(product.id) 87 | print(cart_item.id) -------------------------------------------------------------------------------- /web-shop/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BiteRide 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 42 | 43 | 44 |
45 |
LOGO
46 | 47 |
48 | 49 | 50 |
51 | 52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 |
60 |

Cool Products

61 |

62 | 63 | 64 | 65 |

66 |
67 | 68 | 69 | 70 |
71 | 72 | 73 | Grafana 74 |
75 | {% block title %} 76 | {% endblock %} 77 |

{% block titlebutton %}{% endblock %}

78 |
79 |
80 | 81 | 82 | {% block content %}{% endblock %} 83 | 84 | 85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /kubernetes/mariadb.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ##################################### 3 | ############# mariadb ############### 4 | ##################################### 5 | kind: Secret 6 | apiVersion: v1 7 | metadata: 8 | namespace: web-shop-app 9 | labels: 10 | app: mariadb 11 | name: mariadb-config 12 | type: Opaque 13 | stringData: 14 | init.sql: | 15 | CREATE TABLE `customer` ( 16 | `id` int(11) NOT NULL AUTO_INCREMENT, 17 | `name` varchar(50) NOT NULL, 18 | `created_date` datetime NOT NULL DEFAULT current_timestamp(), 19 | PRIMARY KEY (`id`) 20 | ); 21 | 22 | CREATE TABLE `product` ( 23 | `id` int(11) NOT NULL AUTO_INCREMENT, 24 | `name` varchar(50) NOT NULL, 25 | `price` float NOT NULL, 26 | `tag` varchar(5000) NOT NULL, 27 | `pic_ref` varchar(5000) NOT NULL, 28 | PRIMARY KEY (`id`) 29 | ); 30 | 31 | CREATE TABLE `cart` ( 32 | `id` int(11) NOT NULL AUTO_INCREMENT, 33 | `customer_id` int(11) NOT NULL, 34 | `is_deleted` tinyint(1) NOT NULL, 35 | `created_date` datetime NOT NULL DEFAULT current_timestamp(), 36 | `updated_date` datetime DEFAULT NULL, 37 | PRIMARY KEY (`id`), 38 | KEY `customer_id` (`customer_id`), 39 | CONSTRAINT `cart_ibfk_1` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`) 40 | ); 41 | 42 | CREATE TABLE `cart_item` ( 43 | `id` int(11) NOT NULL AUTO_INCREMENT, 44 | `cart_id` int(11) DEFAULT NULL, 45 | `product_id` int(11) DEFAULT NULL, 46 | `quantity` int(11) NOT NULL, 47 | `is_deleted` tinyint(1) NOT NULL, 48 | `created_date` datetime NOT NULL DEFAULT current_timestamp(), 49 | `updated_date` datetime DEFAULT NULL, 50 | PRIMARY KEY (`id`), 51 | KEY `cart_id` (`cart_id`), 52 | KEY `product_id` (`product_id`), 53 | CONSTRAINT `cart_item_ibfk_1` FOREIGN KEY (`cart_id`) REFERENCES `cart` (`id`), 54 | CONSTRAINT `cart_item_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) 55 | ); 56 | 57 | CREATE TABLE `order_table` ( 58 | `id` int(11) NOT NULL AUTO_INCREMENT, 59 | `cart_id` int(11) DEFAULT NULL, 60 | `order_uuid` varchar(200) NOT NULL, 61 | `created_date` datetime NOT NULL DEFAULT current_timestamp(), 62 | `updated_date` datetime DEFAULT NULL, 63 | PRIMARY KEY (`id`), 64 | KEY `cart_id` (`cart_id`), 65 | CONSTRAINT `order_table_ibfk_1` FOREIGN KEY (`cart_id`) REFERENCES `cart` (`id`) 66 | ); 67 | 68 | delete from cart_item; 69 | delete from cart; 70 | delete from customer; 71 | delete from product; 72 | delete from order_table; 73 | 74 | INSERT INTO product (name, price, tag, pic_ref) VALUES ("Meows", 29.99,"","/static/img/cats/Meows.jpeg"); 75 | INSERT INTO product (name, price, tag, pic_ref) VALUES ("Loki", 39.99,"","/static/img/cats/Loki.jpeg"); 76 | INSERT INTO product (name, price, tag, pic_ref) VALUES ("Charlie", 19.50,"","/static//img/cats/Charlie.jpeg"); 77 | INSERT INTO product (name, price, tag, pic_ref) VALUES ("Carla", 25.00,"","/static/img/cats/Carla.jpeg"); 78 | 79 | --- 80 | apiVersion: v1 81 | kind: Service 82 | metadata: 83 | namespace: web-shop-app 84 | labels: 85 | app: mariadb 86 | name: mariadb 87 | spec: 88 | ports: 89 | - name: "core" 90 | port: 3306 91 | targetPort: 3306 92 | selector: 93 | app: mariadb 94 | --- 95 | apiVersion: apps/v1 96 | kind: Deployment 97 | metadata: 98 | namespace: web-shop-app 99 | labels: 100 | app: mariadb 101 | name: mariadb 102 | spec: 103 | replicas: 1 104 | selector: 105 | matchLabels: 106 | app: mariadb 107 | template: 108 | metadata: 109 | labels: 110 | app: mariadb 111 | spec: 112 | containers: 113 | - name: mariadb 114 | image: docker.io/mariadb:10.9.4 115 | env: 116 | - name: MARIADB_ROOT_PASSWORD 117 | value: myrootpassword 118 | - name: MARIADB_DATABASE 119 | value: webshop 120 | command: 121 | ports: 122 | - containerPort: 3306 123 | volumeMounts: 124 | - mountPath: /var/lib/mysql 125 | name: data 126 | - mountPath: /docker-entrypoint-initdb.d 127 | name: mariadb-config 128 | livenessProbe: 129 | tcpSocket: 130 | port: 3306 131 | initialDelaySeconds: 30 132 | periodSeconds: 10 133 | timeoutSeconds: 5 134 | failureThreshold: 6 135 | successThreshold: 1 136 | readinessProbe: 137 | tcpSocket: 138 | port: 3306 139 | initialDelaySeconds: 5 140 | periodSeconds: 10 141 | timeoutSeconds: 5 142 | failureThreshold: 6 143 | successThreshold: 1 144 | volumes: 145 | - name: data 146 | emptyDir: {} 147 | - name: mariadb-config 148 | secret: 149 | secretName: mariadb-config 150 | 151 | -------------------------------------------------------------------------------- /agent/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | log_level: debug 3 | 4 | 5 | integrations: 6 | metrics: 7 | autoscrape: 8 | enable: true 9 | metrics_instance: mimir 10 | 11 | agent: 12 | # The Agent dashboards are written to assume Kubernetes, so we inject some 13 | # fake Kubernetes labels here. 14 | extra_labels: 15 | cluster: docker-compose 16 | namespace: docker-compose 17 | container: grafana-agent 18 | pod: grafana-agent-${HOSTNAME:-example} 19 | 20 | app_agent_receiver_configs: 21 | - autoscrape: 22 | enable: true 23 | metrics_instance: 'mimir' 24 | instance: 'frontend' 25 | logs_instance: 'loki' 26 | traces_instance: 'tempo' 27 | server: 28 | host: 0.0.0.0 29 | port: 12347 30 | cors_allowed_origins: 31 | - ${APP_ENDPOINT} 32 | api_key: 'secret' # optional, if set, client will be required to provide it via x-api-key header 33 | logs_labels: # labels to add to loki log record 34 | app: 'web-shop-frontend' # static value 35 | kind: # value will be taken from log items. exception, log, measurement, etc 36 | logs_send_timeout: 5000 37 | sourcemaps: 38 | download: true # will download source file, extract source map location, 39 | 40 | metrics: 41 | wal_directory: /var/lib/agent/wal 42 | global: 43 | scrape_interval: 15s 44 | external_labels: 45 | cluster: web-shop 46 | configs: 47 | - name: mimir 48 | remote_write: 49 | - url: http://mimir:9009/api/v1/push 50 | scrape_configs: 51 | - job_name: 'backends' 52 | 53 | # Override the global default and scrape targets from this job every 5 seconds. 54 | scrape_interval: 5s 55 | 56 | static_configs: 57 | - targets: ['localhost:9009'] 58 | 59 | - job_name: "node" 60 | static_configs: 61 | - targets: ["node-exporter:9100"] 62 | 63 | - job_name: "jmx" 64 | static_configs: 65 | - targets: ["products:8081"] 66 | 67 | - job_name: 'web-shop' 68 | scrape_interval: 5s 69 | 70 | static_configs: 71 | - targets: ['web-shop:6666'] 72 | labels: 73 | group: 'shop' 74 | service: 'web-shop' 75 | 76 | - job_name: 'shopping-cart' 77 | scrape_interval: 5s 78 | 79 | static_configs: 80 | - targets: ['shopping-cart:5555'] 81 | labels: 82 | group: 'shop' 83 | service: 'shopping-cart' 84 | 85 | - job_name: 'products' 86 | scrape_interval: 5s 87 | metrics_path: /actuator/prometheus 88 | static_configs: 89 | - targets: ['products:8080'] 90 | labels: 91 | group: 'shop' 92 | service: 'products' 93 | 94 | - job_name: 'discounter' 95 | scrape_interval: 5s 96 | static_configs: 97 | - targets: ['discounter:3036'] 98 | labels: 99 | group: 'shop' 100 | service: 'discounter' 101 | 102 | - job_name: 'span-metrics' 103 | scrape_interval: 5s 104 | static_configs: 105 | - targets: ['agent:1234'] 106 | labels: 107 | group: 'shop' 108 | 109 | 110 | - job_name: 'blackbox' 111 | metrics_path: /probe 112 | params: 113 | module: [http_2xx] # Look for a HTTP 200 response. 114 | static_configs: 115 | - targets: 116 | - http://web-shop:6666/cart 117 | - http://web-shop:6666/shop 118 | - http://products:8080 119 | - http://shopping-cart:5555/cart/blackbox 120 | - https://example.com 121 | - https://www.telegraph.co.uk 122 | relabel_configs: 123 | - source_labels: [__address__] 124 | target_label: __param_target 125 | - source_labels: [__param_target] 126 | target_label: instance 127 | - target_label: __address__ 128 | replacement: blackbox_exporter:9115 129 | 130 | 131 | 132 | logs: 133 | configs: 134 | - name: loki 135 | clients: 136 | - url: http://loki:3100/loki/api/v1/push 137 | external_labels: 138 | job: agent 139 | positions_directory: /tmp/positions 140 | 141 | traces: 142 | configs: 143 | - name: tempo 144 | receivers: 145 | otlp: 146 | protocols: 147 | grpc: 148 | http: 149 | remote_write: 150 | - endpoint: tempo:4317 151 | insecure: true 152 | #service_graphs: 153 | # enabled: true 154 | #spanmetrics: 155 | # dimensions: 156 | # - name: http.method 157 | # - name: http.target 158 | # - name: team.name 159 | # handler_endpoint: 0.0.0.0:1234 160 | automatic_logging: 161 | backend: logs_instance 162 | logs_instance_name: loki 163 | roots: true 164 | processes: true 165 | spans: true 166 | span_attributes: 167 | - http.method 168 | - http.target 169 | - http.status_code 170 | overrides: 171 | trace_id_key: "traceId" 172 | -------------------------------------------------------------------------------- /web-shop/static/main.js: -------------------------------------------------------------------------------- 1 | // Raw JavaScript 2 | var appAgentReceiverEndpoint = document.getElementById("variables").dataset.appAgentReceiverEndpoint; 3 | console.log(appAgentReceiverEndpoint); 4 | 5 | const webSdkScript = document.createElement('script'); 6 | 7 | webSdkScript.src = 'https://unpkg.com/@grafana/faro-web-sdk@^1.0.0/dist/bundle/faro-web-sdk.iife.js'; 8 | 9 | webSdkScript.onload = () => { 10 | window.GrafanaFaroWebSdk.initializeFaro({ 11 | url: appAgentReceiverEndpoint, 12 | app: { 13 | name: 'web-shop-browser', 14 | version: '1.6.0', 15 | environment: 'production', 16 | }, 17 | }); 18 | 19 | const webTracingScript = document.createElement('script'); 20 | webTracingScript.src = 'https://unpkg.com/@grafana/faro-web-tracing@^1.0.0/dist/bundle/faro-web-tracing.iife.js'; 21 | webTracingScript.onload = () => { 22 | window.GrafanaFaroWebSdk.faro.instrumentations.add(new window.GrafanaFaroWebTracing.TracingInstrumentation({ 23 | // Optional, if you want to add custom attributes to the resource 24 | resourceAttributes: { 25 | "service.name": "web-shop-browser", 26 | "team.name": "browser" 27 | }, 28 | })); 29 | }; 30 | document.head.appendChild(webTracingScript); 31 | }; 32 | 33 | document.head.appendChild(webSdkScript); 34 | 35 | 36 | //export const {trace, context} = faro.api.getOTEL(); 37 | 38 | //Accordion 39 | function myAccFunc() { 40 | var x = document.getElementById("demoAcc"); 41 | if (x.className.indexOf("w3-show") == -1) { 42 | x.className += " w3-show"; 43 | console.log("accordeon opened") 44 | var rand = Math.random(); 45 | faro.api.pushMeasurement({ 46 | type: "accordeon measurments", 47 | values: { 48 | accordeon_pressure: rand, 49 | }, 50 | }); 51 | faro.api.pushLog(['accordeon opened, faro']); 52 | } else { 53 | x.className = x.className.replace(" w3-show", ""); 54 | console.log("accordeon closed") 55 | faro.api.pushLog(['accordeon closed, faro']); 56 | } 57 | } 58 | 59 | // document.getElementById("myBtn").click(); 60 | 61 | function w3_open() { 62 | document.getElementById("mySidebar").style.display = "block"; 63 | document.getElementById("myOverlay").style.display = "block"; 64 | } 65 | 66 | function w3_close() { 67 | document.getElementById("mySidebar").style.display = "none"; 68 | document.getElementById("myOverlay").style.display = "none"; 69 | } 70 | 71 | function buy_now(product_name, person_name){ 72 | var xhr = new XMLHttpRequest(); 73 | xhr.open("POST", "/shop?product=" + product_name + "&name=" + person_name, true); 74 | xhr.setRequestHeader('Content-Type', 'application/json'); 75 | xhr.send(JSON.stringify({ 76 | "product_name": product_name, 77 | "person": person_name 78 | })); 79 | //window.alert('Successfully added item to shopping cart. Add more items or pay the shopping cart (right upper corner)') 80 | } 81 | 82 | function empty_cart(person_name){ 83 | var xhr = new XMLHttpRequest(); 84 | xhr.open("POST", "/cart?name=" + person_name, true); 85 | xhr.setRequestHeader('Content-Type', 'application/json'); 86 | xhr.send(JSON.stringify({ 87 | "person": person_name 88 | })); 89 | window.alert('shopping cart emptied'); 90 | location.reload(); 91 | } 92 | 93 | function checkout_cart(person_name){ 94 | var xhr = new XMLHttpRequest(); 95 | var rand = Math.random(); 96 | if (rand < 0.7){ 97 | xhr.open("POST", "/cart?name=" + person_name + "&checkout=true", true); 98 | xhr.setRequestHeader('Content-Type', 'application/json'); 99 | xhr.send(JSON.stringify({ 100 | "person": person_name, 101 | "checkout": true 102 | })); 103 | console.log("Successfully checked out."); 104 | window.alert("Successfully checked out. Please come again and buy some more stuff."); 105 | } 106 | else if (rand < 0.8) { 107 | faro.api.pushError(new Error("ERROR CODE 9999: Mainframe on fire!")); 108 | window.alert("ERROR CODE 999: Mainframe on fire! Please try again in a few seconds."); 109 | } 110 | else if (rand < 0.9) { 111 | faro.api.pushError(new Error("ERROR CODE 2023: Happy new year!")); 112 | window.alert("ERROR CODE 2023: Happy new year! Please try again in a few seconds."); 113 | } 114 | else if (rand <= 1) { 115 | faro.api.pushError(new Error("ERROR CODE 2412: Feliz Navidad.")); 116 | window.alert("ERROR CODE 24.12: Feliz Navidad. Please try again in a few seconds."); 117 | } 118 | location.reload(); 119 | } 120 | 121 | function apply_discount(person_name){ 122 | var xhr = new XMLHttpRequest(); 123 | xhr.open("POST", "/cart?name=" + person_name + "&discount=true", true); 124 | xhr.setRequestHeader('Content-Type', 'application/json'); 125 | xhr.send(JSON.stringify({ 126 | "person": person_name, 127 | "discount": true 128 | })); 129 | window.alert("You're too cheap to be eligible for a discount! Please go to amazon to buy your stuff."); 130 | location.reload(); 131 | } 132 | 133 | 134 | 135 | window.myAccFunc = myAccFunc; 136 | window.w3_open = w3_open; 137 | window.w3_close = w3_close; 138 | window.buy_now = buy_now; 139 | window.empty_cart = empty_cart; 140 | window.checkout_cart = checkout_cart; 141 | window.apply_discount = apply_discount; 142 | -------------------------------------------------------------------------------- /grafana/dashboards/business/Customer_View.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "target": { 12 | "limit": 100, 13 | "matchAny": false, 14 | "tags": [], 15 | "type": "dashboard" 16 | }, 17 | "type": "dashboard" 18 | } 19 | ] 20 | }, 21 | "editable": true, 22 | "fiscalYearStartMonth": 0, 23 | "graphTooltip": 0, 24 | "id": 2, 25 | "links": [], 26 | "liveNow": false, 27 | "panels": [ 28 | { 29 | "datasource": { 30 | "type": "mysql", 31 | "uid": "P211906C1C32DB77E" 32 | }, 33 | "fieldConfig": { 34 | "defaults": { 35 | "color": { 36 | "mode": "palette-classic" 37 | }, 38 | "custom": { 39 | "axisLabel": "", 40 | "axisPlacement": "auto", 41 | "axisSoftMin": 0, 42 | "fillOpacity": 80, 43 | "gradientMode": "none", 44 | "hideFrom": { 45 | "legend": false, 46 | "tooltip": false, 47 | "viz": false 48 | }, 49 | "lineWidth": 1, 50 | "scaleDistribution": { 51 | "type": "linear" 52 | } 53 | }, 54 | "mappings": [], 55 | "thresholds": { 56 | "mode": "absolute", 57 | "steps": [ 58 | { 59 | "color": "green", 60 | "value": null 61 | }, 62 | { 63 | "color": "red", 64 | "value": 80 65 | } 66 | ] 67 | }, 68 | "unit": "currencyUSD" 69 | }, 70 | "overrides": [ 71 | { 72 | "matcher": { 73 | "id": "byName", 74 | "options": "sum(cart_item.quantity * product.price)" 75 | }, 76 | "properties": [ 77 | { 78 | "id": "color", 79 | "value": { 80 | "mode": "continuous-BlPu" 81 | } 82 | } 83 | ] 84 | } 85 | ] 86 | }, 87 | "gridPos": { 88 | "h": 9, 89 | "w": 12, 90 | "x": 0, 91 | "y": 0 92 | }, 93 | "id": 2, 94 | "options": { 95 | "barRadius": 0, 96 | "barWidth": 0.97, 97 | "groupWidth": 0.7, 98 | "legend": { 99 | "calcs": [], 100 | "displayMode": "hidden", 101 | "placement": "bottom" 102 | }, 103 | "orientation": "auto", 104 | "showValue": "auto", 105 | "stacking": "none", 106 | "tooltip": { 107 | "mode": "single", 108 | "sort": "none" 109 | }, 110 | "xTickLabelRotation": 0, 111 | "xTickLabelSpacing": 0 112 | }, 113 | "targets": [ 114 | { 115 | "datasource": { 116 | "type": "mysql", 117 | "uid": "P211906C1C32DB77E" 118 | }, 119 | "format": "table", 120 | "group": [], 121 | "metricColumn": "none", 122 | "rawQuery": true, 123 | "rawSql": "SELECT customer.name, sum(cart_item.quantity * product.price)\n FROM cart_item \n LEFT JOIN product\n ON cart_item.product_id = product.id\n LEFT JOIN cart ON cart_item.cart_id = cart.id\n LEFT JOIN customer ON cart.customer_id = customer.id\n GROUP BY customer.name ORDER BY sum(cart_item.quantity * product.price) DESC LIMIT 5;", 124 | "refId": "A", 125 | "select": [ 126 | [ 127 | { 128 | "params": [ 129 | "value" 130 | ], 131 | "type": "column" 132 | } 133 | ] 134 | ], 135 | "timeColumn": "time", 136 | "where": [ 137 | { 138 | "name": "$__timeFilter", 139 | "params": [], 140 | "type": "macro" 141 | } 142 | ] 143 | } 144 | ], 145 | "title": "Top 5 Biggest Current Shopping Carts Before Checkout", 146 | "transformations": [], 147 | "transparent": true, 148 | "type": "barchart" 149 | }, 150 | { 151 | "datasource": { 152 | "type": "loki", 153 | "uid": "loki" 154 | }, 155 | "gridPos": { 156 | "h": 11, 157 | "w": 12, 158 | "x": 0, 159 | "y": 9 160 | }, 161 | "id": 4, 162 | "options": { 163 | "dedupStrategy": "none", 164 | "enableLogDetails": true, 165 | "prettifyLogMessage": false, 166 | "showCommonLabels": false, 167 | "showLabels": false, 168 | "showTime": true, 169 | "sortOrder": "Descending", 170 | "wrapLogMessage": false 171 | }, 172 | "targets": [ 173 | { 174 | "datasource": { 175 | "type": "loki", 176 | "uid": "loki" 177 | }, 178 | "expr": "{service=\"products\"} |= \"com.datahovel.webshop.Application\" | pattern \"<_> com.datahovel.webshop.Application - traceID=<_>\" | line_format \"{{.cart_items}}\" | json | __error__ = \"\" | line_format \"Customer: {{.cart_customer_name}} | Product: {{.product_name }} | Price: {{.quantity}}*{{.product_price}}\"", 179 | "refId": "A" 180 | } 181 | ], 182 | "title": "Checked out Shopping Cart Items", 183 | "type": "logs" 184 | } 185 | ], 186 | "refresh": "5s", 187 | "schemaVersion": 35, 188 | "style": "dark", 189 | "tags": [], 190 | "templating": { 191 | "list": [] 192 | }, 193 | "time": { 194 | "from": "now-6h", 195 | "to": "now" 196 | }, 197 | "timepicker": {}, 198 | "timezone": "", 199 | "title": "Customer View", 200 | "uid": "MYCRtOPnz", 201 | "version": 1, 202 | "weekStart": "" 203 | } -------------------------------------------------------------------------------- /web-shop/routes.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template, redirect, url_for, session, flash 2 | import json 3 | import requests 4 | from flask import current_app as app 5 | import uuid 6 | 7 | shopping_cart_url = "http://shopping-cart:5555" 8 | products_url = "http://products:8080" 9 | 10 | 11 | @app.route('/cart', methods=["GET", "POST"]) 12 | def view_cart(): 13 | person = request.args.get("name") 14 | checkout = request.args.get("checkout") 15 | discount = request.args.get("discount") 16 | headers = {'Content-type': 'application/json'} 17 | request_string = "{}/cart/{}".format(shopping_cart_url, person) 18 | 19 | 20 | 21 | if request.method == "POST" and discount: 22 | apply_discount(person, headers, request_string) 23 | 24 | if request.method == "POST" and not discount: 25 | check_out_cart(checkout, headers, request_string, person) 26 | 27 | items = get_items_from_shopping_cart(request_string) 28 | app.logger.info(app.app_agent_receiver_endpoint) 29 | return render_template('cart.html', items=items, person=person, app_agent_receiver_endpoint=app.app_agent_receiver_endpoint) 30 | 31 | @app.route('/shop', methods=["GET", "POST"]) 32 | def view_shop(): 33 | person = request.args.get("name") 34 | #person = session['username'] 35 | product_name = request.args.get("product") 36 | headers = {'Content-type': 'application/json'} 37 | request_string = "{}/products/".format(products_url) 38 | 39 | products = get_products(request_string) 40 | 41 | if request.method == "POST" and product_name: 42 | add_to_shopping_cart(person, product_name, headers) 43 | 44 | #flash("session id" + session.get("id")) 45 | 46 | app.logger.info("Showing web interface.") 47 | return render_template('index.html', products=products, person=person, app_agent_receiver_endpoint=app.app_agent_receiver_endpoint) 48 | 49 | def add_to_shopping_cart(person, product_name, headers): 50 | request_string = "{}/cart/{}".format(shopping_cart_url, person) 51 | payload = {"product": product_name} 52 | response = requests.post(request_string, json=payload,headers=headers) 53 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 54 | app.logger.exception("Got a real bad response from shopping cart. Something is wrong.") 55 | else: 56 | app.logger.info("Successfully added item to shopping cart.") 57 | 58 | def get_products(request_string): 59 | response = requests.get(request_string) 60 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 61 | app.logger.exception("Got a real bad response from products service. Something is wrong.") 62 | else: 63 | app.logger.info("Retrieved available items from products service. Displaying items.") 64 | products = response.json() 65 | app.logger.info("Successfully obtained items from shopping cart") 66 | return products 67 | 68 | def check_out_cart(checkout, headers, request_string, person): 69 | items = get_items_from_shopping_cart(request_string) 70 | if checkout: 71 | order_uuid = uuid.uuid4() 72 | order_request_string = "{}/order/{}".format(shopping_cart_url, order_uuid) 73 | payload = json.loads('{"name": "' + person + '"}') 74 | app.logger.debug("Trying to create order {}".format(order_uuid)) 75 | response = requests.post(order_request_string, json=payload, headers=headers) 76 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 77 | app.logger.exception("Could not create order {} ".format(order_uuid)) 78 | else: 79 | app.logger.info("Created order {}.".format(order_uuid)) 80 | 81 | for item in items: 82 | product_request_string = "{}/products/checkout".format(products_url) 83 | payload = item 84 | response = requests.post(product_request_string, json=payload, headers=headers ) 85 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 86 | app.logger.exception("Could not check out item.") 87 | else: 88 | app.logger.info("Checked out item.") 89 | response = requests.delete(request_string, headers=headers) 90 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 91 | app.logger.exception("Got a real bad response from shopping cart. Something is wrong.") 92 | else: 93 | app.logger.info("Successfully emptied shopping cart.") 94 | 95 | def apply_discount(person, headers, request_string): 96 | request_string_discount = request_string + "/discount" 97 | response = requests.post(request_string_discount,json={"name": person}, headers=headers) 98 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 99 | app.logger.exception("Bad response from discount service. HTTP Status Code: {}".format(response.status_code)) 100 | else: 101 | app.logger.info("discount applied successfully.") 102 | 103 | def get_items_from_shopping_cart(request_string): 104 | response = requests.get(request_string) 105 | if not (response.status_code == 200 or response.status_code == 201 or response.status_code == 202): 106 | items = [] 107 | app.logger.exception("Got a real bad response from shopping cart. Something is wrong.") 108 | else: 109 | items = response.json() 110 | app.logger.info("Successfully obtained items from shopping cart") 111 | return items 112 | 113 | # Route for handling the login page logic 114 | @app.route('/login', methods=['GET', 'POST']) 115 | def login(): 116 | error = None 117 | if request.method == 'POST': 118 | session['username'] = request.form['username'] 119 | return redirect(url_for('view_shop', name=request.form['username'])) 120 | return render_template('login.html', error=error) 121 | 122 | 123 | if __name__ == '__main__': 124 | app.run(debug=False, host='0.0.0.0',port=3389) 125 | -------------------------------------------------------------------------------- /setup/add_products.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # desc: Load kittens into the product catalog 3 | # params: 4 | # return (status code/stdout): 5 | load_kittens() { 6 | echo "Loading some kittens into the product catalog..." 7 | echo " loading Meows" 8 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Meows", "price": "29.99", "tag": "cool", "pic_ref": "https://placekitten.com/251/250"}' localhost:8080/products/ >& add_products.sh.log 9 | echo " loading Loki" 10 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Loki", "price": "39.99", "tag": "", "pic_ref": "https://placekitten.com/251/251"}' localhost:8080/products/ >& add_products.sh.log 11 | echo " loading Charlie" 12 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Charlie", "price": "19.50", "tag": "special", "pic_ref": "https://placekitten.com/250/251"}' localhost:8080/products/ >& add_products.sh.log 13 | echo " loading Carla" 14 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Carla", "price": "25.00", "tag": "special", "pic_ref": "https://placekitten.com/249/250"}' localhost:8080/products/ >& add_products.sh.log 15 | echo "Done!" 16 | } 17 | 18 | # desc: Load smartphones into the product catalog 19 | # params: 20 | # return (status code/stdout): 21 | load_phones() { 22 | echo "Loading some phones into the product catalog..." 23 | echo " loading a XIAOMI phone" 24 | curl -X POST -H "Content-Type: application/json" -d '{"name": "XIAOMI", "price": "275.00", "tag": "5g", "pic_ref": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTKBmH_aC5CmItXprFLTRKpev6HLiUesYoPIA&usqp=CAU"}' localhost:8080/products/ >& add_products.sh.log 25 | echo " loading a ZTE phone" 26 | curl -X POST -H "Content-Type: application/json" -d '{"name": "ZTE", "price": "175.00", "tag": "4g", "pic_ref": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR3wu0GHlwzpPPlh0KgQlMIxkY8h7zs7USPIw&usqp=CAU"}' localhost:8080/products/ >& add_products.sh.log 27 | echo " loading a OPPO phone" 28 | curl -X POST -H "Content-Type: application/json" -d '{"name": "OPPO", "price": "255.00", "tag": "5g", "pic_ref": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTO1kvevSM1v2ltBdB9ApnOU6GTYsVxT9jYLQ&usqp=CAU"}' localhost:8080/products/ >& add_products.sh.log 29 | echo " loading a SAMSUNG phone" 30 | curl -X POST -H "Content-Type: application/json" -d '{"name": "SAMSUNG", "price": "155.00", "tag": "5g", "pic_ref": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRmO3FEddg3lQD1s8f1xdSVselUhHc7ntRvEg&usqp=CAU"}' localhost:8080/products/ >& add_products.sh.log 31 | echo "Done!" 32 | } 33 | 34 | # desc: Load energy products into the product catalog 35 | # params: 36 | # return (status code/stdout): 37 | load_energy_products() { 38 | echo "Loading some energy products into the product catalog..." 39 | echo " loading a solar panel" 40 | curl -X POST -H "Content-Type: application/json" -d '{"name": "SolarPanel", "price": "275.00", "tag": "cheap now!", "pic_ref": "/static/img/energy/solarpanel.png"}' localhost:8080/products/ >& add_products.sh.log 41 | echo "Done!" 42 | echo " loading a electric car charger" 43 | curl -X POST -H "Content-Type: application/json" -d '{"name": "CarChargeBox", "price": "777.00", "tag": "hot", "pic_ref": "/static/img/energy/wallbox.png"}' localhost:8080/products/ >& add_products.sh.log 44 | echo "Done!" 45 | echo " loading a modem" 46 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Internet", "price": "20.00", "tag": "", "pic_ref": "/static/img/energy/modem.png"}' localhost:8080/products/ >& add_products.sh.log 47 | echo "Done!" 48 | echo " loading a heatpump" 49 | curl -X POST -H "Content-Type: application/json" -d '{"name": "HeatPump", "price": "2499.00", "tag": "save", "pic_ref": "/static/img/energy/heatpump.png"}' localhost:8080/products/ >& add_products.sh.log 50 | echo "Done!" 51 | } 52 | 53 | # desc: Load food products into the product catalog 54 | # params: 55 | # return (status code/stdout): 56 | load_food_products() { 57 | echo "Loading some energy products into the product catalog..." 58 | echo " loading a salmon" 59 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Salmon", "price": "12.90", "tag": "fishy!", "pic_ref": "/static/img/food/salmon.png"}' localhost:8080/products/ >& add_products.sh.log 60 | echo "Done!" 61 | echo " loading a pasta" 62 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Pasta", "price": "9.99", "tag": "hot", "pic_ref": "/static/img/food/pasta.png"}' localhost:8080/products/ >& add_products.sh.log 63 | echo "Done!" 64 | echo " loading a chicken" 65 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Chicken", "price": "14.90", "tag": "", "pic_ref": "/static/img/food/chicken.png"}' localhost:8080/products/ >& add_products.sh.log 66 | echo "Done!" 67 | echo " loading a shrimps" 68 | curl -X POST -H "Content-Type: application/json" -d '{"name": "Shrimps", "price": "19.00", "tag": "save now", "pic_ref": "/static/img/food/shrimps.png"}' localhost:8080/products/ >& add_products.sh.log 69 | echo "Done!" 70 | } 71 | 72 | # desc: display script's syntax 73 | # params: 74 | # return (status code/stdout): 75 | usage() { 76 | echo "Usage: add_product.sh [VERTICAL]" 77 | echo 78 | echo "Available verticals for add_product:" 79 | echo " default it loads some kittens" 80 | echo " phones it loads some smartphones" 81 | echo " energy it loads some energy products" 82 | echo " food it loads some food products" 83 | } 84 | 85 | # eval_args 86 | # desc: evaluate the arguments provided to the script 87 | # params: 88 | # $1 - option to execute 89 | # return (status code/stdout): 90 | # 0/ok message - the option is executed properly 91 | # 1/ko message - display usage due to a syntax error 92 | eval_args(){ 93 | if [ $# -eq 1 ] 94 | then 95 | if [ "$1" == "default" ] 96 | then 97 | load_kittens 98 | elif [ "$1" == "phones" ] 99 | then 100 | load_phones 101 | elif [ "$1" == "energy" ] 102 | then 103 | load_energy_products 104 | elif [ "$1" == "food" ] 105 | then 106 | load_food_products 107 | else 108 | echo "Error: bad vertical" 109 | usage 110 | exit 1 111 | fi 112 | else 113 | echo "Error: bad number of arguments" 114 | usage 115 | exit 1 116 | fi 117 | } 118 | 119 | # Script's entry point 120 | # 121 | if ! [ -z "$*" ] 122 | then 123 | eval_args $* 124 | exit 0 125 | fi 126 | 127 | usage 128 | exit 1 129 | -------------------------------------------------------------------------------- /products/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Shop Observability Demo 2 | 3 | This is a simple demo to show metrics, logs and traces collection and visualisation with the Grafana stack in a distributed microservices system. 4 | Feel free to check out the code to all of the services and notice how it is instrumented to scrape and collect the telemetry. 5 | 6 | ## Overview 7 | 8 | This demo spins up a simplified containerized "web shop" service. 9 | 10 | Currently, it consists of: 11 | * web-shop: a user interface that allows you to add items to a shopping cart, as well as delete all items in the shopping cart. 12 | * shopping-cart: a backend service that interacts with a MariaDB instance. It persists the shopping cart items of the different users. 13 | * products: a backend service that serves the available products of the web shop. 14 | * mariadb: A mariadb instance to persist products and shopping cart items. 15 | * broker: a kafka broker to persist checked out shopping carts before they are reset. 16 | 17 | 18 | 19 | Additionally, you have the required agents and instrumentation included as well as the backends to collect metrics, logs and traces: 20 | * grafana: a Grafana instance to view and build dashboards, and explore the collected telemetry, as well as to create alerts on them. 21 | * agent: a Grafana Agent to scrape metrics, collect logs and traces 22 | * mimir: Grafana Mimir to act as the metrics backend 23 | * loki: Grafana Loki to act as the logs backend 24 | * tempo: Grafana Tempo to persist trace spans 25 | * blackbox_exporter: an exporter to expose uptime metric of the services 26 | * node_exporter: an exporter to expose metrics of the underlying infrastructure 27 | 28 | ## Architecture 29 | ![](images/web-shop-architecture.png) 30 | Quick Overview: 31 | * The shop simulator service simulates user traffic on top of the web shop UI. 32 | * The web shop UI is a Python Flask service that renders 2 HTML pages: the shop landing page as well as the shopping cart view. The shop landing page loads products by requesting them from the products API. The shopping cart view interacts with the shopping cart service to get the current shopping cart items from the user. 33 | * The shopping cart service is written in Flask and offers an API to interact with MariaDB. 34 | * The products service is written in Java Spring Boot and offers an API to load the currently available shop items from MariaDB. 35 | * The products servie additionally has a Kafka producer and consumer implemented. The producer will send the content of the shopping cart as JSON to a Kafka topic. The consumer simply logs the message 36 | * Telemetry is instrumented by using some of the available python otel libraries. It's collected using the OTEL collector of the Grafana Agent and then sent to Tempo. 37 | * The Java autoinstrumentation is performed using the javaagent. 38 | * The Grafana Agent acts as an OTEL collected and is configured to scrape the web shop service as well as the shopping cart API and the products API for Prometheus metrics. 39 | * Logs are collected using the Loki docker plugin. The plugin needs to be installed before starting the demo. Docker compose points to the local Loki container to persist the logs. 40 | 41 | ## How To get started 42 | 43 | There are two options to deploy this demo. One option is to run it in a K8s cluster and send all the telemetry to Grafana Cloud. The other option is to run everything locally in docker-compose. 44 | 45 | 46 | ### Prerequisites 47 | * I've tested this on a Ubuntu machine, but it should run on any Linux based system using `systemctl`. 48 | * It gets stuck on a M1 MacBook, so I recommend a VM with 2+ cores and 4GB RAM. 49 | * `git`, `wget`, `docker` and `docker-compose` commands should be installed and accessible without `sudo` 50 | * Also `docker-compose` shouldn't be any older than `1.29.2` 51 | 52 | ### Installation 53 | * Step 0: 54 | ``` 55 | git clone https://github.com/Condla/web-shop-o11y-demo.git 56 | cd web-shop-o11y-demo 57 | ``` 58 | 59 | * Step 1: 60 | * Make sure you forward the port 3000 to your localhost (or run docker-compose on your local machine). This will be used to access Grafana. 61 | * Make sure you forward the port 3389 to your localhost (or run docker-compose on your local machine). This will be used to access the web application. 62 | * Make sure you forward the port 12347 to your localhost (or run docker-compose on your local machine). This is where the app agent receiver listens to front end telemetry sent directly from the browser. 63 | 64 | > *Note*: Optionally you can set the environment variable `PUBLIC_APP_URL`. This should be set to the URL you are running the script from. 65 | >```export PUBLIC_APP_URL=grafana.datahovel.com``` 66 | > In this case you need to make sure that the ports 3000, 3389 and 12347 are accessible from the public internet. 67 | 68 | * Step 2: Run the up script which will start the application including the observability platform in the background. 69 | ``` 70 | /bin/bash up.sh 71 | ``` 72 | 73 | * Step 3: Lean back. The download of all docker images will take a while the first time you run this. 74 | 75 | ## Next Steps 76 | 77 | Regardless of where you have deployed the app and the agents/backends you'll now have access to the web application accessible via port 3389. 78 | 79 | * Go to `:3389/shop?name=` to see the web shop interface. 80 | * If you didn't add any products the shop should be empty. 81 | * You can run the script below to add 4 kittens to the web shop: 82 | ``` 83 | # this will load kittens to the web shop 84 | setup/add_products.sh default 85 | 86 | # alternatively load some phones to the web shop 87 | # setup/add_products.sh phones 88 | ``` 89 | 90 | * If you refresh the web shop. You should be able to see the 4 new products in the shop now. 91 | 92 | ![](images/web-shop-ui.png) 93 | 94 | ## Grafana Access 95 | 96 | * Grafana is available via Grafana Cloud or running in docker-compose on port 3000. The default username password combination for the docker-compose instance is `admin/grafana`. 97 | * Go to the dashboards menu and open the "Holistic Webshop Monitoring" dashboard that gives you an overview including a drill down of the webshop services. 98 | 99 | ![](images/web-shop-dashboard.png) 100 | 101 | 102 | ## Run the realistic user simulation. 103 | 104 | * To start a user simulation using k6 just run 105 | ```./simulate-user-traffic.sh``` 106 | * To stop the test press `Ctrl+C` 107 | * This will create a bit of traffic on the web shop and allows you to start observing normal and strange patterns of the application with the help of Grafana. 108 | 109 | ## What this demo should demonstrate 110 | 111 | There's multiple ways you could use this demo instance. There's multiple errors built in the simulation, the proxy is configured to reach a timeout to simulate proxy timeouts and many other scenarios to show how you can use the LGTM stack to solve issues. 112 | 113 | I plan to publish some demo flows here. 114 | -------------------------------------------------------------------------------- /complete-demo/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | web-shop: 5 | driver: bridge 6 | 7 | services: 8 | 9 | grafana: 10 | image: grafana/grafana 11 | volumes: 12 | - "./grafana/definitions:/var/lib/grafana/dashboards" 13 | - "./grafana/provisioning:/etc/grafana/provisioning" 14 | ports: 15 | - "3000:3000" 16 | environment: 17 | GF_FEATURE_TOGGLES_ENABLE: "tempoSearch tempoServiceGraph" 18 | GF_INSTALL_PLUGINS: grafana-k6-app 19 | networks: 20 | - web-shop 21 | 22 | prometheus: 23 | build: ./prometheus 24 | ports: 25 | - "9090" 26 | networks: 27 | - web-shop 28 | 29 | node-exporter: 30 | image: prom/node-exporter:latest 31 | container_name: node-exporter 32 | volumes: 33 | - /proc:/host/proc:ro 34 | - /sys:/host/sys:ro 35 | - /:/rootfs:ro 36 | command: 37 | - '--path.procfs=/host/proc' 38 | - '--path.rootfs=/rootfs' 39 | - '--path.sysfs=/host/sys' 40 | - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' 41 | ports: 42 | - 9100 43 | networks: 44 | - web-shop 45 | 46 | loki: 47 | image: grafana/loki:latest 48 | ports: 49 | - "3100:3100" 50 | networks: 51 | - web-shop 52 | 53 | shopping-cart: 54 | image: "condla/shopping-cart:1.1" 55 | ports: 56 | - "5555:5555" 57 | environment: 58 | SQLALCHEMY_DATABASE_URI: mariadb+mariadbconnector://root:myrootpassword@mariadb:3306/webshop 59 | OTEL_EXPORTER_OTLP_ENDPOINT: http://agent:4317 60 | depends_on: 61 | mariadb: 62 | condition: service_healthy 63 | 64 | networks: 65 | - web-shop 66 | logging: 67 | driver: loki 68 | options: 69 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 70 | loki-relabel-config: | 71 | - action: labelmap 72 | regex: compose_(service) 73 | 74 | mariadb: 75 | image: mariadb:latest 76 | ports: 77 | - "3306:3306" 78 | environment: 79 | MARIADB_ROOT_PASSWORD: myrootpassword 80 | MARIADB_DATABASE: webshop 81 | networks: 82 | - web-shop 83 | logging: 84 | driver: loki 85 | options: 86 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 87 | loki-relabel-config: | 88 | - action: labelmap 89 | regex: compose_(service) 90 | healthcheck: 91 | # test: ["mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pmyrootpassword"] 92 | test: mysqladmin ping -h 127.0.0.1 -uroot -pmyrootpassword 93 | interval: 10s 94 | timeout: 10s 95 | retries: 5 96 | start_period: 30s 97 | 98 | agent: 99 | image: grafana/agent:latest 100 | ports: 101 | - "80" 102 | - "6832" 103 | - "55679" 104 | - "4317:4317" 105 | - "1234:1234" 106 | volumes: 107 | - "${PWD}/agent/config.yaml:/etc/agent/agent.yaml" 108 | networks: 109 | - web-shop 110 | 111 | tempo: 112 | image: grafana/tempo:latest 113 | ports: 114 | - "3200" 115 | - "4317" 116 | - "55680" 117 | - "55681" 118 | - "14250" 119 | command: [ "-config.file=/etc/tempo.yaml"] 120 | volumes: 121 | - ./tempo/tempo.yaml:/etc/tempo.yaml 122 | networks: 123 | - web-shop 124 | 125 | products: 126 | image: "condla/products:otel-1.0" 127 | ports: 128 | - "8080:8080" 129 | environment: 130 | OTEL_METRICS_EXPORTER: "none" 131 | OTEL_EXPORTER: "otlp_span" 132 | OTEL_EXPORTER_OTLP_ENDPOINT: "http://agent:4317" 133 | OTEL_RESOURCE_ATTRIBUTES: "service.name=products,team.name=backend,environment=production" 134 | JAVA_OPTS: "-javaagent:/opentelemetry-javaagent.jar" 135 | networks: 136 | - web-shop 137 | depends_on: 138 | - "shopping-cart" 139 | - "broker" 140 | logging: 141 | driver: loki 142 | options: 143 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 144 | loki-relabel-config: | 145 | - action: labelmap 146 | regex: compose_(service) 147 | 148 | web-shop: 149 | image: "condla/web-shop:1.1" 150 | ports: 151 | - "3389:6666" 152 | environment: 153 | OTEL_EXPORTER_OTLP_ENDPOINT: "http://agent:4317" 154 | depends_on: 155 | - "shopping-cart" 156 | networks: 157 | - web-shop 158 | restart: on-failure 159 | logging: 160 | driver: loki 161 | options: 162 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 163 | loki-relabel-config: | 164 | - action: labelmap 165 | regex: compose_(service) 166 | 167 | zookeeper: 168 | image: confluentinc/cp-zookeeper:7.0.1 169 | hostname: zookeeper 170 | container_name: zookeeper 171 | ports: 172 | - "2181:2181" 173 | environment: 174 | ZOOKEEPER_CLIENT_PORT: 2181 175 | ZOOKEEPER_TICK_TIME: 2000 176 | networks: 177 | - web-shop 178 | 179 | broker: 180 | image: confluentinc/cp-server:7.0.1 181 | hostname: broker 182 | container_name: broker 183 | depends_on: 184 | - zookeeper 185 | ports: 186 | - "9092:9092" 187 | - "9101:9101" 188 | environment: 189 | KAFKA_BROKER_ID: 1 190 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 191 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 192 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 193 | KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter 194 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 195 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 196 | KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 197 | KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 198 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 199 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 200 | KAFKA_JMX_PORT: 9101 201 | KAFKA_JMX_HOSTNAME: localhost 202 | KAFKA_CREATE_TOPICS: audit:1:1,networkevents:1:1,webserverlogs:1:1,dbcommits:1:1,machine1:1:1,machine2:1:1 203 | #KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 204 | CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 205 | CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 206 | CONFLUENT_METRICS_ENABLE: 'true' 207 | CONFLUENT_SUPPORT_CUSTOMER_ID: 'anonymous' 208 | networks: 209 | - web-shop 210 | 211 | # shop-simulator: 212 | # image: "condla/shop-simulator:1.0" 213 | # depends_on: 214 | # - "web-shop" 215 | # networks: 216 | # - web-shop 217 | # restart: on-failure 218 | 219 | blackbox_exporter: 220 | image: prom/blackbox-exporter:master 221 | volumes: 222 | - ./blackbox_exporter:/config 223 | command: --config.file=/config/blackbox.yml 224 | ports: 225 | - 9115:9115 226 | -------------------------------------------------------------------------------- /products/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | echo Found %WRAPPER_JAR% 133 | ) else ( 134 | if not "%MVNW_REPOURL%" == "" ( 135 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" 136 | ) 137 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 138 | echo Downloading from: %DOWNLOAD_URL% 139 | 140 | powershell -Command "&{"^ 141 | "$webclient = new-object System.Net.WebClient;"^ 142 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 143 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 144 | "}"^ 145 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 146 | "}" 147 | echo Finished downloading %WRAPPER_JAR% 148 | ) 149 | @REM End of extension 150 | 151 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 152 | if ERRORLEVEL 1 goto error 153 | goto end 154 | 155 | :error 156 | set ERROR_CODE=1 157 | 158 | :end 159 | @endlocal & set ERROR_CODE=%ERROR_CODE% 160 | 161 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 162 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 163 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 164 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 165 | :skipRcPost 166 | 167 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 168 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 169 | 170 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 171 | 172 | exit /B %ERROR_CODE% 173 | -------------------------------------------------------------------------------- /complete-demo/docker-compose.yml.bakcup: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | web-shop: 5 | driver: bridge 6 | 7 | services: 8 | 9 | grafana: 10 | image: grafana/grafana 11 | volumes: 12 | - "./grafana/definitions:/var/lib/grafana/dashboards" 13 | - "./grafana/provisioning:/etc/grafana/provisioning" 14 | ports: 15 | - "3000:3000" 16 | environment: 17 | GF_FEATURE_TOGGLES_ENABLE: "tempoSearch tempoServiceGraph publicDashboards" 18 | GF_INSTALL_PLUGINS: grafana-k6-app 19 | networks: 20 | - web-shop 21 | 22 | node-exporter: 23 | image: prom/node-exporter:latest 24 | container_name: node-exporter 25 | volumes: 26 | - /proc:/host/proc:ro 27 | - /sys:/host/sys:ro 28 | - /:/rootfs:ro 29 | command: 30 | - '--path.procfs=/host/proc' 31 | - '--path.rootfs=/rootfs' 32 | - '--path.sysfs=/host/sys' 33 | - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' 34 | ports: 35 | - 9100 36 | networks: 37 | - web-shop 38 | 39 | loki: 40 | image: grafana/loki:latest 41 | ports: 42 | - "3100:3100" 43 | networks: 44 | - web-shop 45 | 46 | mimir: 47 | image: grafana/mimir:latest 48 | ports: 49 | - "9009:9009" 50 | volumes: 51 | - "./mimir/demo.yaml:/etc/mimir/demo.yaml" 52 | command: 53 | - '--config.file=/etc/mimir/demo.yaml' 54 | networks: 55 | - web-shop 56 | 57 | shopping-cart: 58 | build: "./shopping-cart" 59 | ports: 60 | - "5555:5555" 61 | environment: 62 | SQLALCHEMY_DATABASE_URI: mariadb+mariadbconnector://root:myrootpassword@mariadb:3306/webshop 63 | OTEL_EXPORTER_OTLP_ENDPOINT: http://agent:4317 64 | depends_on: 65 | mariadb: 66 | condition: service_healthy 67 | 68 | networks: 69 | - web-shop 70 | logging: 71 | driver: loki 72 | options: 73 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 74 | loki-relabel-config: | 75 | - action: labelmap 76 | regex: compose_(service) 77 | 78 | mariadb: 79 | image: mariadb:latest 80 | ports: 81 | - "3306:3306" 82 | environment: 83 | MARIADB_ROOT_PASSWORD: myrootpassword 84 | MARIADB_DATABASE: webshop 85 | networks: 86 | - web-shop 87 | logging: 88 | driver: loki 89 | options: 90 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 91 | loki-relabel-config: | 92 | - action: labelmap 93 | regex: compose_(service) 94 | healthcheck: 95 | # test: ["mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pmyrootpassword"] 96 | test: mysqladmin ping -h 127.0.0.1 -uroot -pmyrootpassword 97 | interval: 10s 98 | timeout: 10s 99 | retries: 5 100 | start_period: 30s 101 | 102 | agent: 103 | image: grafana/agent:latest 104 | ports: 105 | - "80" 106 | - "6832" 107 | - "55679" 108 | - "4317:4317" 109 | - "1234:1234" 110 | volumes: 111 | - "${PWD}/agent/config.yaml:/etc/agent/agent.yaml" 112 | networks: 113 | - web-shop 114 | logging: 115 | driver: loki 116 | options: 117 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 118 | loki-relabel-config: | 119 | - action: labelmap 120 | regex: compose_(service) 121 | 122 | tempo: 123 | image: grafana/tempo:latest 124 | ports: 125 | - "3200" 126 | - "4317" 127 | - "55680" 128 | - "55681" 129 | - "14250" 130 | command: [ "-config.file=/etc/tempo.yaml"] 131 | volumes: 132 | - ./tempo/tempo.yaml:/etc/tempo.yaml 133 | networks: 134 | - web-shop 135 | 136 | products: 137 | image: "condla/products:otel-1.0" 138 | ports: 139 | - "8080:8080" 140 | environment: 141 | OTEL_METRICS_EXPORTER: "none" 142 | OTEL_EXPORTER: "otlp_span" 143 | OTEL_EXPORTER_OTLP_ENDPOINT: "http://agent:4317" 144 | OTEL_RESOURCE_ATTRIBUTES: "service.name=products,team.name=backend,environment=production" 145 | JAVA_OPTS: "-javaagent:/opentelemetry-javaagent.jar" 146 | networks: 147 | - web-shop 148 | depends_on: 149 | - "shopping-cart" 150 | - "broker" 151 | logging: 152 | driver: loki 153 | options: 154 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 155 | loki-relabel-config: | 156 | - action: labelmap 157 | regex: compose_(service) 158 | 159 | web-shop: 160 | build: "./web-shop" 161 | ports: 162 | - "3389:6666" 163 | environment: 164 | OTEL_EXPORTER_OTLP_ENDPOINT: "http://agent:4317" 165 | depends_on: 166 | - "shopping-cart" 167 | networks: 168 | - web-shop 169 | restart: on-failure 170 | logging: 171 | driver: loki 172 | options: 173 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 174 | loki-relabel-config: | 175 | - action: labelmap 176 | regex: compose_(service) 177 | 178 | zookeeper: 179 | image: confluentinc/cp-zookeeper:7.0.1 180 | hostname: zookeeper 181 | container_name: zookeeper 182 | ports: 183 | - "2181:2181" 184 | environment: 185 | ZOOKEEPER_CLIENT_PORT: 2181 186 | ZOOKEEPER_TICK_TIME: 2000 187 | networks: 188 | - web-shop 189 | 190 | broker: 191 | image: confluentinc/cp-server:7.0.1 192 | hostname: broker 193 | container_name: broker 194 | depends_on: 195 | - zookeeper 196 | ports: 197 | - "9092:9092" 198 | - "9101:9101" 199 | environment: 200 | KAFKA_BROKER_ID: 1 201 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 202 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 203 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 204 | KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter 205 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 206 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 207 | KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 208 | KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 209 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 210 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 211 | KAFKA_JMX_PORT: 9101 212 | KAFKA_JMX_HOSTNAME: localhost 213 | KAFKA_CREATE_TOPICS: audit:1:1,networkevents:1:1,webserverlogs:1:1,dbcommits:1:1,machine1:1:1,machine2:1:1 214 | #KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 215 | CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 216 | CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 217 | CONFLUENT_METRICS_ENABLE: 'true' 218 | CONFLUENT_SUPPORT_CUSTOMER_ID: 'anonymous' 219 | networks: 220 | - web-shop 221 | 222 | # shop-simulator: 223 | # image: "condla/shop-simulator:1.0" 224 | # depends_on: 225 | # - "web-shop" 226 | # networks: 227 | # - web-shop 228 | # restart: on-failure 229 | 230 | blackbox_exporter: 231 | image: prom/blackbox-exporter:master 232 | volumes: 233 | - ./blackbox_exporter:/config 234 | command: --config.file=/config/blackbox.yml 235 | ports: 236 | - 9115:9115 237 | 238 | squid: 239 | image: ubuntu/squid 240 | ports: 241 | - "3128:3128" 242 | restart: unless-stopped 243 | volumes: 244 | - ./squid/squid.conf:/etc/squid/squid.conf 245 | environment: 246 | TZ: UTC 247 | networks: 248 | - web-shop 249 | logging: 250 | driver: loki 251 | options: 252 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 253 | loki-relabel-config: | 254 | - action: labelmap 255 | regex: compose_(service) 256 | 257 | 258 | -------------------------------------------------------------------------------- /shopping-cart/routes.py: -------------------------------------------------------------------------------- 1 | from flask import request, jsonify, Response 2 | from flask import current_app as app 3 | import logging, json 4 | from models import db, Customer, Cart, CartItem, Product, Order 5 | import time 6 | import random 7 | 8 | def get_or_setup_customer(customer_name): 9 | customer = 0 10 | try: 11 | logging.debug("Check for customer in database.") 12 | customer = Customer.query.filter_by(name=customer_name).first() 13 | except: 14 | logging.exception("Customer couldn't be queried from database.") 15 | 16 | if not customer: 17 | logging.info("No Customer with this name found. Creating new Customer.") 18 | customer = Customer(name=customer_name) 19 | try: 20 | db.session.add(customer) 21 | db.session.commit() 22 | except: 23 | logging.exception("Customer couldn't be created due to database exception.") 24 | return customer 25 | 26 | def get_or_setup_cart(customer): 27 | try: 28 | logging.debug("Look up existing cart of customer") 29 | cart = Cart.query.filter_by(customer=customer, is_deleted=False).first() 30 | except: 31 | logging.exception("Shopping cart couldn't be looked up due to database exception.") 32 | if not cart: 33 | logging.info("No active shopping cart for this customer found. Creating new cart.") 34 | cart = Cart(customer=customer) 35 | try: 36 | db.session.add(cart) 37 | db.session.commit() 38 | except: 39 | logging.exception("Shopping cart couldn't be created due to database exception.") 40 | return cart 41 | 42 | @app.route('/cart/', methods=["GET"]) 43 | def get_cart(customer_name): 44 | response_object = {"response": "Exception"} 45 | status_code = 500 46 | customer = get_or_setup_customer(customer_name) 47 | cart = get_or_setup_cart(customer) 48 | cart_items = [] 49 | try: 50 | cart_items = CartItem.query.filter_by(cart=cart, is_deleted=False).all() 51 | # cart_items = CartItem.query.filter_by(cart=cart)\ 52 | # .join(Cart, Cart.id == CartItem.cart_id)\ 53 | # .join(Customer, Customer.id == Cart.customer_id)\ 54 | # .join(Product, Product.id == CartItem.product_id)\ 55 | # .add_columns(Product.name).all() 56 | response_object = [cart_item for cart_item in cart_items] 57 | status_code = 200 58 | except: 59 | logging.exception("Could not retrieve cart items due to database exception.") 60 | logging.info("Successfully retrieved shopping cart of customer.") 61 | return jsonify(response_object), status_code 62 | 63 | @app.route('/cartitems/', methods=["GET"]) 64 | def get_cart_items(customer_name): 65 | response_object = {"response": "Exception"} 66 | status_code = 500 67 | customer = get_or_setup_customer(customer_name) 68 | cart = get_or_setup_cart(customer) 69 | cart_items = [] 70 | try: 71 | cart_items = CartItem.query.filter_by(cart=cart, is_deleted=False).all() 72 | # cart_items = CartItem.query.filter_by(cart=cart)\ 73 | # .join(Cart, Cart.id == CartItem.cart_id)\ 74 | # .join(Customer, Customer.id == Cart.customer_id)\ 75 | # .join(Product, Product.id == CartItem.product_id)\ 76 | # .add_columns(Product.name).all() 77 | response_object = [cart_item for cart_item in cart_items] 78 | status_code = 200 79 | except: 80 | logging.exception("Could not retrieve cart items due to database exception.") 81 | logging.info("Successfully retrieved shopping cart of customer.") 82 | return jsonify(response_object), status_code 83 | 84 | @app.route('/cart/', methods=["POST"]) 85 | def post_cart_items(customer_name): 86 | response_object = {"response": "Exception"} 87 | status_code = 500 88 | customer = get_or_setup_customer(customer_name) 89 | cart = get_or_setup_cart(customer) 90 | try: 91 | logging.info("Getting payload sent with POST request.") 92 | res = request.get_json() 93 | product_name = res['product'] 94 | except: 95 | logging.exception("There was an issue with the submitted payload. Possibly a request header is missing 'Content-Type: application/json'") 96 | 97 | try: 98 | logging.info("Retrieving product.") 99 | product = Product.query.filter_by(name=product_name).first() 100 | except: 101 | logging.exception("There was an issue retrieving the product from the database.") 102 | 103 | if not product: 104 | logging.error("Product that you wanted to add to shopping cart could not be found in database.") 105 | return jsonify({"product name": product_name}), status_code 106 | 107 | try: 108 | logging.info("Checking if cart_item already in shopping cart.") 109 | cart_item = CartItem.query.filter_by(product=product, cart=cart, is_deleted=False).first() 110 | except: 111 | logging.exception("There was an issue retrieving cart_item from the database") 112 | if not cart_item: 113 | logging.debug("Cart item not in shopping cart. Creating new one.") 114 | cart_item = CartItem(product=product, cart=cart, quantity=1) 115 | else: 116 | logging.debug("Cart item already in shopping cart. Increasing quantity.") 117 | cart_item.quantity += 1 118 | try: 119 | db.session.add(cart_item) 120 | db.session.commit() 121 | response_object = cart_item 122 | status_code = 201 123 | except: 124 | logging.exception("Could not persist cart item in database.") 125 | 126 | logging.info("Successfully added item to shopping cart.") 127 | return jsonify(response_object), status_code 128 | 129 | @app.route('/order/', methods=["POST"]) 130 | def create_order(order_uuid): 131 | response_object = {"response": "Exception"} 132 | status_code = 500 133 | try: 134 | logging.info("Getting payload sent with POST request.") 135 | res = request.get_json() 136 | customer_name = res['name'] 137 | except: 138 | logging.exception("There was an issue with the submitted payload. Possibly a request header is missing 'Content-Type: application/json'") 139 | customer = get_or_setup_customer(customer_name) 140 | cart = get_or_setup_cart(customer) 141 | 142 | order = Order(cart=cart, order_uuid=order_uuid) 143 | try: 144 | db.session.add(order) 145 | db.session.commit() 146 | response_object = order 147 | status_code = 201 148 | except: 149 | logging.exception("Could not persist order {} in database.".format(order_uuid)) 150 | 151 | logging.info("Successfully created order {}.".format(order_uuid)) 152 | return jsonify(response_object), status_code 153 | 154 | @app.route('/cart/', methods=["DELETE"]) 155 | def delete_cart_items(customer_name): 156 | response_object = {"response": "Exception"} 157 | status_code = 500 158 | customer = get_or_setup_customer(customer_name) 159 | cart = get_or_setup_cart(customer) 160 | 161 | ### delete all items from shopping cart: 162 | try: 163 | #cart_items = db.session.query(CartItem).filter_by(cart=cart).delete(synchronize_session=False) 164 | logging.info("Trying to delete all items from shopping cart.") 165 | cart_items = db.session.query(CartItem).filter_by(cart=cart).update({"is_deleted": True}, synchronize_session=False) 166 | db.session.commit() 167 | response_object = cart_items 168 | status_code = 200 169 | except: 170 | logging.exception("Could not delete items from shopping cart due to a database error.") 171 | 172 | ### additionally delete shopping cart itself: 173 | try: 174 | logging.info("Trying to delete shopping cart.") 175 | cart = db.session.query(Cart).filter_by(id=cart.id).update({"is_deleted": True}, synchronize_session=False) 176 | db.session.commit() 177 | status_code = 200 178 | except: 179 | logging.exception("Could not delete shopping cart due to a database error.") 180 | logging.info("Successfully deleted shopping cart of user.") 181 | return jsonify(response_object), status_code 182 | 183 | 184 | @app.route('/cart//discount', methods=["POST"]) 185 | def apply_discount(customer_name): 186 | response_object = {"response": "Discount applied!"} 187 | status_code = 200 188 | logging.info("Successfully applied discount.") 189 | if random.randint(1,12) == 1: 190 | time.sleep(10) 191 | return jsonify(response_object), status_code -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | networks: 4 | web-shop: 5 | driver: bridge 6 | 7 | services: 8 | 9 | grafana: 10 | image: grafana/grafana:9.4.7 11 | volumes: 12 | - "./grafana/dashboards:/var/lib/grafana/dashboards" 13 | - "./grafana/conf:/etc/grafana/" 14 | ports: 15 | - "3000:3000" 16 | environment: 17 | GF_FEATURE_TOGGLES_ENABLE: "publicDashboards ngalert accesscontrol panelTitleSearch tempoSearch tempoApmTable traceToMetrics recordedQueries traceqlEditor flameGraph topnav" 18 | GF_INSTALL_PLUGINS: "grafana-k6-app,grafana-polystat-panel" 19 | networks: 20 | - web-shop 21 | logging: 22 | driver: loki 23 | options: 24 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 25 | loki-relabel-config: | 26 | - action: labelmap 27 | regex: compose_(service) 28 | 29 | mimir: 30 | image: grafana/mimir:2.5.0 31 | ports: 32 | - "9009:9009" 33 | volumes: 34 | - "./mimir/demo.yaml:/etc/mimir/demo.yaml" 35 | command: 36 | - '--config.file=/etc/mimir/demo.yaml' 37 | networks: 38 | - web-shop 39 | logging: 40 | driver: loki 41 | options: 42 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 43 | loki-relabel-config: | 44 | - action: labelmap 45 | regex: compose_(service) 46 | 47 | loki: 48 | image: grafana/loki:2.7.2 49 | ports: 50 | - "3100:3100" 51 | networks: 52 | - web-shop 53 | logging: 54 | driver: loki 55 | options: 56 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 57 | loki-relabel-config: | 58 | - action: labelmap 59 | regex: compose_(service) 60 | 61 | tempo: 62 | image: grafana/tempo:2.0.0 63 | ports: 64 | - "3200" 65 | - "4317" 66 | - "55680" 67 | - "55681" 68 | - "14250" 69 | command: [ "-config.file=/etc/tempo.yaml"] 70 | volumes: 71 | - ./tempo/tempo.yaml:/etc/tempo.yaml 72 | networks: 73 | - web-shop 74 | logging: 75 | driver: loki 76 | options: 77 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 78 | loki-relabel-config: | 79 | - action: labelmap 80 | regex: compose_(service) 81 | 82 | agent: 83 | image: grafana/agent:v0.31.0 84 | entrypoint: 85 | - /bin/agent 86 | - -config.file=/etc/agent/agent.yaml 87 | - -enable-features=integrations-next 88 | - -config.expand-env 89 | - -config.enable-read-api 90 | ports: 91 | - "80" 92 | - "6832" 93 | - "55679" 94 | - "4317:4317" 95 | - "1234:1234" 96 | - "12347:12347" 97 | volumes: 98 | - "./agent/config.yaml:/etc/agent/agent.yaml" 99 | networks: 100 | - web-shop 101 | environment: 102 | APP_ENDPOINT: http://${PUBLIC_APP_URL}:3389 103 | logging: 104 | driver: loki 105 | options: 106 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 107 | loki-relabel-config: | 108 | - action: labelmap 109 | regex: compose_(service) 110 | 111 | web-shop: 112 | image: "condla/web-shop:2.1-grafana-cloud" 113 | #build: "./web-shop" 114 | ports: 115 | - "3389:3389" 116 | environment: 117 | OTEL_EXPORTER_OTLP_ENDPOINT: "http://agent:4317" 118 | APP_AGENT_RECEIVER_ENDPOINT: "http://${PUBLIC_APP_URL}:12347" 119 | depends_on: 120 | - "shopping-cart" 121 | networks: 122 | - web-shop 123 | restart: on-failure 124 | logging: 125 | driver: loki 126 | options: 127 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 128 | loki-relabel-config: | 129 | - action: labelmap 130 | regex: compose_(service) 131 | 132 | shopping-cart: 133 | image: "condla/shopping-cart:1.4" 134 | #build: "./shopping-cart" 135 | ports: 136 | - "5555:5555" 137 | environment: 138 | SQLALCHEMY_DATABASE_URI: mariadb+mariadbconnector://root:myrootpassword@mariadb:3306/webshopdb 139 | OTEL_EXPORTER_OTLP_ENDPOINT: http://agent:4317 140 | depends_on: 141 | mariadb: 142 | condition: service_healthy 143 | networks: 144 | - web-shop 145 | logging: 146 | driver: loki 147 | options: 148 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 149 | loki-relabel-config: | 150 | - action: labelmap 151 | regex: compose_(service) 152 | 153 | products: 154 | image: "condla/products:otel-1.6" 155 | #build: "./products" 156 | ports: 157 | - "8080:8080" 158 | - "8081:8081" 159 | environment: 160 | OTEL_METRICS_EXPORTER: "none" 161 | OTEL_EXPORTER: "otlp_span" 162 | OTEL_EXPORTER_OTLP_ENDPOINT: "http://agent:4317" 163 | OTEL_RESOURCE_ATTRIBUTES: "service.name=products,team.name=backend,environment=production" 164 | OTEL_INSTRUMENTATION_MESSAGING_EXPERIMENTAL_RECEIVE_TELEMETRY_ENABLED: "true" 165 | JAVA_OPTS: "-javaagent:/opentelemetry-javaagent.jar -javaagent:/jmx_prometheus_javaagent.jar=8081:/httpserver.yml" 166 | networks: 167 | - web-shop 168 | depends_on: 169 | - "shopping-cart" 170 | - "broker" 171 | security_opt: 172 | - seccomp:phlare/seccomp.json 173 | logging: 174 | driver: loki 175 | options: 176 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 177 | loki-relabel-config: | 178 | - action: labelmap 179 | regex: compose_(service) 180 | 181 | mariadb: 182 | image: mariadb:latest 183 | ports: 184 | - "3306:3306" 185 | environment: 186 | MARIADB_ROOT_PASSWORD: myrootpassword 187 | MARIADB_DATABASE: webshopdb 188 | networks: 189 | - web-shop 190 | logging: 191 | driver: loki 192 | options: 193 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 194 | loki-relabel-config: | 195 | - action: labelmap 196 | regex: compose_(service) 197 | healthcheck: 198 | # test: ["mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pmyrootpassword"] 199 | test: mysqladmin ping -h 127.0.0.1 -uroot -pmyrootpassword 200 | interval: 10s 201 | timeout: 10s 202 | retries: 5 203 | start_period: 30s 204 | 205 | broker: 206 | image: confluentinc/cp-server:7.0.1 207 | hostname: broker 208 | container_name: broker 209 | depends_on: 210 | - zookeeper 211 | ports: 212 | - "9092:9092" 213 | - "9101:9101" 214 | environment: 215 | KAFKA_BROKER_ID: 1 216 | KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' 217 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 218 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 219 | KAFKA_METRIC_REPORTERS: io.confluent.metrics.reporter.ConfluentMetricsReporter 220 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 221 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 222 | KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 223 | KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 224 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 225 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 226 | KAFKA_JMX_PORT: 9101 227 | KAFKA_JMX_HOSTNAME: localhost 228 | KAFKA_CREATE_TOPICS: audit:1:1,networkevents:1:1,webserverlogs:1:1,dbcommits:1:1,machine1:1:1,machine2:1:1 229 | #KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 230 | CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 231 | CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 232 | CONFLUENT_METRICS_ENABLE: 'true' 233 | CONFLUENT_SUPPORT_CUSTOMER_ID: 'anonymous' 234 | networks: 235 | - web-shop 236 | logging: 237 | driver: loki 238 | options: 239 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 240 | loki-relabel-config: | 241 | - action: labelmap 242 | regex: compose_(service) 243 | 244 | zookeeper: 245 | image: confluentinc/cp-zookeeper:7.0.1 246 | hostname: zookeeper 247 | container_name: zookeeper 248 | ports: 249 | - "2181:2181" 250 | environment: 251 | ZOOKEEPER_CLIENT_PORT: 2181 252 | ZOOKEEPER_TICK_TIME: 2000 253 | networks: 254 | - web-shop 255 | logging: 256 | driver: loki 257 | options: 258 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 259 | loki-relabel-config: | 260 | - action: labelmap 261 | regex: compose_(service) 262 | 263 | squid: 264 | image: ubuntu/squid 265 | ports: 266 | - "3128:3128" 267 | restart: unless-stopped 268 | volumes: 269 | - ./squid/squid.conf:/etc/squid/squid.conf 270 | environment: 271 | TZ: UTC 272 | networks: 273 | - web-shop 274 | logging: 275 | driver: loki 276 | options: 277 | loki-url: "http://172.17.0.1:3100/loki/api/v1/push" 278 | loki-relabel-config: | 279 | - action: labelmap 280 | regex: compose_(service) 281 | 282 | node-exporter: 283 | image: prom/node-exporter:latest 284 | container_name: node-exporter 285 | volumes: 286 | - /proc:/host/proc:ro 287 | - /sys:/host/sys:ro 288 | - /:/rootfs:ro 289 | command: 290 | - '--path.procfs=/host/proc' 291 | - '--path.rootfs=/rootfs' 292 | - '--path.sysfs=/host/sys' 293 | - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' 294 | ports: 295 | - 9100 296 | networks: 297 | - web-shop 298 | 299 | # blackbox_exporter: 300 | # image: prom/blackbox-exporter:master 301 | # volumes: 302 | # - ./blackbox_exporter:/config 303 | # command: --config.file=/config/blackbox.yml 304 | # ports: 305 | # - 9115:9115 306 | 307 | 308 | -------------------------------------------------------------------------------- /shop-simulator/webshop-browser-and-protocol-demo.js: -------------------------------------------------------------------------------- 1 | import { sleep, group, check } from 'k6' 2 | import http from 'k6/http' 3 | import { chromium } from 'k6/x/browser'; 4 | import exec from 'k6/execution'; 5 | import { SharedArray } from 'k6/data'; 6 | import { vu } from 'k6/execution'; 7 | 8 | let user= "Emil"; 9 | 10 | export const options = { 11 | scenarios: { 12 | front_end_scenario: { 13 | executor: 'constant-vus', 14 | exec: 'browser', 15 | vus: 1, 16 | duration: '1m', 17 | }, 18 | back_end_scenario: { 19 | executor: 'constant-vus', 20 | exec: 'backend', 21 | vus: 10, 22 | duration: '1m', 23 | }, 24 | }, 25 | }; 26 | 27 | export function browser() { 28 | const browser = chromium.launch({ 29 | headless: false, 30 | slowMo: '500ms', 31 | }); 32 | 33 | const context = browser.newContext({ 34 | screen: {width: 1024, height: 768}, 35 | viewport: { width: 1024, height: 768} 36 | }); 37 | const page = context.newPage(); 38 | page.goto(`http://${__ENV.HOSTNAME}:3389/shop?name=`+user); 39 | 40 | let selector, elem; 41 | 42 | page.mouse.click(344, 402); 43 | sleep(1); 44 | page.mouse.click(530, 410); 45 | sleep(1); 46 | 47 | selector='[href="/cart?name='+user+'"]'; 48 | elem = page.$(selector); 49 | elem.click(); 50 | 51 | sleep(1); 52 | 53 | selector='.w3-main'; 54 | elem = page.$(selector); 55 | 56 | let re= /Loki\s*(.*)\s*(.*)\s*(.*)\s*Meows\s*(.*)\s*(.*)\s*(.*)\s*/; 57 | let match = elem.textContent().match(re); 58 | if(match== null) { 59 | re= /Meows\s*(.*)\s*(.*)\s*(.*)\s*Loki\s*(.*)\s*(.*)\s*(.*)\s*/; 60 | match = elem.textContent().match(re); 61 | } 62 | 63 | let total= Number(match[3])+Number(match[6]); 64 | console.log("Total: "+total); 65 | 66 | check(page, { in: "69.98"== total}); 67 | 68 | sleep(1); 69 | 70 | // page.screenshot({ path:"kitties.png" }); 71 | 72 | selector='[name="checkout_cart"]'; 73 | elem = page.$(selector); 74 | elem.click(); 75 | 76 | sleep(1); 77 | 78 | selector="[id='myBtn']"; 79 | elem = page.$(selector); 80 | elem.click(); 81 | sleep(1); 82 | elem.click(); 83 | sleep(1); 84 | 85 | selector="[onclick='do_something_cool()']"; 86 | elem = page.$(selector); 87 | elem.click(); 88 | sleep(1); 89 | 90 | page.close(); 91 | browser.close(); 92 | } 93 | 94 | 95 | 96 | 97 | 98 | 99 | const users = new SharedArray('shop users', function() { 100 | return [ 101 | {"username": "Stefan", "cat": "Loki"}, 102 | {"username": "Carlos", "cat": "Thor"}, 103 | {"username": "Raul", "cat": "Loki"}, 104 | {"username": "Abdelkrim", "cat": "Loki"}, 105 | {"username": "Willie", "cat": "Loki"}, 106 | {"username": "Aengus", "cat": "Loki"}, 107 | {"username": "Ward", "cat": "Loki"}, 108 | {"username": "Emil", "cat": "Loki"}, 109 | {"username": "Dave", "cat": "Loki"}, 110 | {"username": "Cyril", "cat": "Loki"}, 111 | {"username": "Devin", "cat": "Loki"}, 112 | {"username": "Mattias", "cat": "Loki"}, 113 | {"username": "Alain", "cat": "Loki"}, 114 | {"username": "Nabeel", "cat": "Loki"}, 115 | {"username": "Kris", "cat": "Loki"}, 116 | {"username": "Konrad", "cat": "Loki"}, 117 | {"username": "Federica", "cat": "Loki"}, 118 | {"username": "Simon", "cat": "Loki"}, 119 | {"username": "Andreas", "cat": "Loki"}, 120 | {"username": "Hans", "cat": "Loki"}, 121 | {"username": "Matt", "cat": "Meows"}, 122 | {"username": "Jake", "cat": "Loki"}, 123 | {"username": "Franka", "cat": "Loki"}, 124 | {"username": "Abdi", "cat": "Meows"} 125 | ] 126 | }); 127 | 128 | export function backend() { 129 | let response 130 | 131 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, function () { 132 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, { 133 | headers: { 134 | 'upgrade-insecure-requests': '1', 135 | }, 136 | }) 137 | sleep(2.6) 138 | }) 139 | 140 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/shop?product=Meows&name=${users[vu.idInTest -1].username}`, function () { 141 | response = http.post( 142 | `http://${__ENV.HOSTNAME}:3389/shop?product=Meows&name=${users[vu.idInTest -1].username}`, 143 | { 144 | product: `Meows`, 145 | }, 146 | { 147 | headers: { 148 | 'content-type': 'application/x-www-form-urlencoded', 149 | origin: 'http://${__ENV.HOSTNAME}:3389', 150 | 'upgrade-insecure-requests': '1', 151 | }, 152 | } 153 | ) 154 | sleep(1.6) 155 | }) 156 | 157 | group(`page_3 - http://${__ENV.HOSTNAME}:3389/shop?product=Carlos&name=${users[vu.idInTest -1].username}`, function () { 158 | response = http.post( 159 | `http://${__ENV.HOSTNAME}:3389/shop?product=${users[vu.idInTest -1].cat}&name=${users[vu.idInTest -1].username}`, 160 | { 161 | product: 'Carlos', 162 | }, 163 | { 164 | headers: { 165 | 'content-type': 'application/x-www-form-urlencoded', 166 | origin: 'http://${__ENV.HOSTNAME}:3389', 167 | 'upgrade-insecure-requests': '1', 168 | }, 169 | } 170 | ) 171 | sleep(1.5) 172 | }) 173 | 174 | group(`page_4 - http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, function () { 175 | response = http.post( 176 | `http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, 177 | { 178 | product: 'Carla', 179 | }, 180 | { 181 | headers: { 182 | 'content-type': 'application/x-www-form-urlencoded', 183 | origin: 'http://${__ENV.HOSTNAME}:3389', 184 | 'upgrade-insecure-requests': '1', 185 | }, 186 | } 187 | ) 188 | sleep(2.3) 189 | }) 190 | 191 | group(`page_5 - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, function () { 192 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, { 193 | headers: { 194 | 'upgrade-insecure-requests': '1', 195 | }, 196 | }) 197 | sleep(3.4) 198 | 199 | response = http.post( 200 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, 201 | { 202 | name: `${users[vu.idInTest -1].username}`, 203 | }, 204 | { 205 | headers: { 206 | 'content-type': 'application/x-www-form-urlencoded', 207 | origin: 'http://${__ENV.HOSTNAME}:3389', 208 | 'upgrade-insecure-requests': '1', 209 | }, 210 | } 211 | ) 212 | sleep(3.1) 213 | }) 214 | 215 | group(`page_6 - http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}#cats`, function () { 216 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, { 217 | headers: { 218 | 'upgrade-insecure-requests': '1', 219 | }, 220 | }) 221 | sleep(1.9) 222 | }) 223 | 224 | group(`page_7 - http://${__ENV.HOSTNAME}:3389/shop?product=Loki&name=${users[vu.idInTest -1].username}`, function () { 225 | response = http.post( 226 | `http://${__ENV.HOSTNAME}:3389/shop?product=Loki&name=${users[vu.idInTest -1].username}`, 227 | { 228 | product: 'Loki', 229 | }, 230 | { 231 | headers: { 232 | 'content-type': 'application/x-www-form-urlencoded', 233 | origin: 'http://${__ENV.HOSTNAME}:3389', 234 | 'upgrade-insecure-requests': '1', 235 | }, 236 | } 237 | ) 238 | sleep(0.8) 239 | }) 240 | 241 | group(`page_8 - http://${__ENV.HOSTNAME}:3389/shop?product=Charlie&name=${users[vu.idInTest -1].username}`, function () { 242 | response = http.post( 243 | `http://${__ENV.HOSTNAME}:3389/shop?product=Charlie&name=${users[vu.idInTest -1].username}`, 244 | { 245 | product: 'Charlie', 246 | }, 247 | { 248 | headers: { 249 | 'content-type': 'application/x-www-form-urlencoded', 250 | origin: 'http://${__ENV.HOSTNAME}:3389', 251 | 'upgrade-insecure-requests': '1', 252 | }, 253 | } 254 | ) 255 | sleep(0.8) 256 | }) 257 | 258 | group(`page_9 - http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, function () { 259 | response = http.post( 260 | `http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, 261 | { 262 | product: 'Carla', 263 | }, 264 | { 265 | headers: { 266 | 'content-type': 'application/x-www-form-urlencoded', 267 | origin: 'http://${__ENV.HOSTNAME}:3389', 268 | 'upgrade-insecure-requests': '1', 269 | }, 270 | } 271 | ) 272 | sleep(1.4) 273 | }) 274 | 275 | group(`page_10 - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, function () { 276 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, { 277 | headers: { 278 | 'upgrade-insecure-requests': '1', 279 | }, 280 | }) 281 | sleep(2.9) 282 | }) 283 | 284 | group(`page_11a - http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, function () { 285 | response = http.post( 286 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&discount=true`, 287 | { 288 | name: `${users[vu.idInTest -1].username}`, 289 | }, 290 | { 291 | headers: { 292 | 'content-type': 'application/x-www-form-urlencoded', 293 | origin: 'http://${__ENV.HOSTNAME}:3389', 294 | 'upgrade-insecure-requests': '1', 295 | }, 296 | } 297 | ) 298 | sleep(1.4) 299 | }) 300 | 301 | group(`page_11b - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&checkout=true`, function () { 302 | response = http.post( 303 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&checkout=true`, 304 | { 305 | name: `${users[vu.idInTest -1].username}`, 306 | }, 307 | { 308 | headers: { 309 | 'content-type': 'application/x-www-form-urlencoded', 310 | origin: 'http://${__ENV.HOSTNAME}:3389', 311 | 'upgrade-insecure-requests': '1', 312 | }, 313 | } 314 | ) 315 | }) 316 | } 317 | -------------------------------------------------------------------------------- /products/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ "$MVNW_REPOURL" = true]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 300 | 301 | exec "$JAVACMD" \ 302 | $MAVEN_OPTS \ 303 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 304 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 305 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 306 | -------------------------------------------------------------------------------- /slos/web-shop-SLO-rules.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | # Code generated by Sloth (v0.11.0): https://github.com/slok/sloth. 4 | # DO NOT EDIT. 5 | 6 | groups: 7 | - name: sloth-slo-sli-recordings-web-shop-endpoint-availability 8 | rules: 9 | - record: slo:sli_error:ratio_rate5m 10 | expr: | 11 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[5m]))) 12 | / 13 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[5m]))) 14 | labels: 15 | owner: sdunkler 16 | repo: condla/web-shop-o11y 17 | sloth_id: web-shop-endpoint-availability 18 | sloth_service: web-shop 19 | sloth_slo: endpoint-availability 20 | sloth_window: 5m 21 | tier: "2" 22 | type: slo 23 | - record: slo:sli_error:ratio_rate30m 24 | expr: | 25 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[30m]))) 26 | / 27 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[30m]))) 28 | labels: 29 | owner: sdunkler 30 | repo: condla/web-shop-o11y 31 | sloth_id: web-shop-endpoint-availability 32 | sloth_service: web-shop 33 | sloth_slo: endpoint-availability 34 | sloth_window: 30m 35 | tier: "2" 36 | type: slo 37 | - record: slo:sli_error:ratio_rate1h 38 | expr: | 39 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[1h]))) 40 | / 41 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[1h]))) 42 | labels: 43 | owner: sdunkler 44 | repo: condla/web-shop-o11y 45 | sloth_id: web-shop-endpoint-availability 46 | sloth_service: web-shop 47 | sloth_slo: endpoint-availability 48 | sloth_window: 1h 49 | tier: "2" 50 | type: slo 51 | - record: slo:sli_error:ratio_rate2h 52 | expr: | 53 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[2h]))) 54 | / 55 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[2h]))) 56 | labels: 57 | owner: sdunkler 58 | repo: condla/web-shop-o11y 59 | sloth_id: web-shop-endpoint-availability 60 | sloth_service: web-shop 61 | sloth_slo: endpoint-availability 62 | sloth_window: 2h 63 | tier: "2" 64 | type: slo 65 | - record: slo:sli_error:ratio_rate6h 66 | expr: | 67 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[6h]))) 68 | / 69 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[6h]))) 70 | labels: 71 | owner: sdunkler 72 | repo: condla/web-shop-o11y 73 | sloth_id: web-shop-endpoint-availability 74 | sloth_service: web-shop 75 | sloth_slo: endpoint-availability 76 | sloth_window: 6h 77 | tier: "2" 78 | type: slo 79 | - record: slo:sli_error:ratio_rate1d 80 | expr: | 81 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[1d]))) 82 | / 83 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[1d]))) 84 | labels: 85 | owner: sdunkler 86 | repo: condla/web-shop-o11y 87 | sloth_id: web-shop-endpoint-availability 88 | sloth_service: web-shop 89 | sloth_slo: endpoint-availability 90 | sloth_window: 1d 91 | tier: "2" 92 | type: slo 93 | - record: slo:sli_error:ratio_rate3d 94 | expr: | 95 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{status_code="STATUS_CODE_ERROR", http_target!="*", http_target=~".+", service=~".+"}[3d]))) 96 | / 97 | (sum by (http_target, service) (rate(traces_spanmetrics_latency_count{ http_target!="*", http_target=~".+", service=~".+"}[3d]))) 98 | labels: 99 | owner: sdunkler 100 | repo: condla/web-shop-o11y 101 | sloth_id: web-shop-endpoint-availability 102 | sloth_service: web-shop 103 | sloth_slo: endpoint-availability 104 | sloth_window: 3d 105 | tier: "2" 106 | type: slo 107 | - record: slo:sli_error:ratio_rate30d 108 | expr: | 109 | sum_over_time(slo:sli_error:ratio_rate5m{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"}[30d]) 110 | / ignoring (sloth_window) 111 | count_over_time(slo:sli_error:ratio_rate5m{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"}[30d]) 112 | labels: 113 | owner: sdunkler 114 | repo: condla/web-shop-o11y 115 | sloth_id: web-shop-endpoint-availability 116 | sloth_service: web-shop 117 | sloth_slo: endpoint-availability 118 | sloth_window: 30d 119 | tier: "2" 120 | type: slo 121 | - name: sloth-slo-meta-recordings-web-shop-endpoint-availability 122 | rules: 123 | - record: slo:objective:ratio 124 | expr: vector(0.94) 125 | labels: 126 | owner: sdunkler 127 | repo: condla/web-shop-o11y 128 | sloth_id: web-shop-endpoint-availability 129 | sloth_service: web-shop 130 | sloth_slo: endpoint-availability 131 | tier: "2" 132 | type: slo 133 | - record: slo:error_budget:ratio 134 | expr: vector(1-0.94) 135 | labels: 136 | owner: sdunkler 137 | repo: condla/web-shop-o11y 138 | sloth_id: web-shop-endpoint-availability 139 | sloth_service: web-shop 140 | sloth_slo: endpoint-availability 141 | tier: "2" 142 | type: slo 143 | - record: slo:time_period:days 144 | expr: vector(30) 145 | labels: 146 | owner: sdunkler 147 | repo: condla/web-shop-o11y 148 | sloth_id: web-shop-endpoint-availability 149 | sloth_service: web-shop 150 | sloth_slo: endpoint-availability 151 | tier: "2" 152 | type: slo 153 | - record: slo:current_burn_rate:ratio 154 | expr: | 155 | slo:sli_error:ratio_rate5m{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} 156 | / on(sloth_id, sloth_slo, sloth_service) group_left 157 | slo:error_budget:ratio{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} 158 | labels: 159 | owner: sdunkler 160 | repo: condla/web-shop-o11y 161 | sloth_id: web-shop-endpoint-availability 162 | sloth_service: web-shop 163 | sloth_slo: endpoint-availability 164 | tier: "2" 165 | type: slo 166 | - record: slo:period_burn_rate:ratio 167 | expr: | 168 | slo:sli_error:ratio_rate30d{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} 169 | / on(sloth_id, sloth_slo, sloth_service) group_left 170 | slo:error_budget:ratio{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} 171 | labels: 172 | owner: sdunkler 173 | repo: condla/web-shop-o11y 174 | sloth_id: web-shop-endpoint-availability 175 | sloth_service: web-shop 176 | sloth_slo: endpoint-availability 177 | tier: "2" 178 | type: slo 179 | - record: slo:period_error_budget_remaining:ratio 180 | expr: 1 - slo:period_burn_rate:ratio{sloth_id="web-shop-endpoint-availability", 181 | sloth_service="web-shop", sloth_slo="endpoint-availability"} 182 | labels: 183 | owner: sdunkler 184 | repo: condla/web-shop-o11y 185 | sloth_id: web-shop-endpoint-availability 186 | sloth_service: web-shop 187 | sloth_slo: endpoint-availability 188 | tier: "2" 189 | type: slo 190 | - record: sloth_slo_info 191 | expr: vector(1) 192 | labels: 193 | owner: sdunkler 194 | repo: condla/web-shop-o11y 195 | sloth_id: web-shop-endpoint-availability 196 | sloth_mode: cli-gen-prom 197 | sloth_objective: "94" 198 | sloth_service: web-shop 199 | sloth_slo: endpoint-availability 200 | sloth_spec: prometheus/v1 201 | sloth_version: v0.11.0 202 | tier: "2" 203 | type: slo 204 | - name: sloth-slo-alerts-web-shop-endpoint-availability 205 | rules: 206 | - alert: Endpoints-HighErrorRate 207 | expr: | 208 | ( 209 | max(slo:sli_error:ratio_rate5m{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (14.4 * 0.06)) without (sloth_window) 210 | and 211 | max(slo:sli_error:ratio_rate1h{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (14.4 * 0.06)) without (sloth_window) 212 | ) 213 | or 214 | ( 215 | max(slo:sli_error:ratio_rate30m{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (6 * 0.06)) without (sloth_window) 216 | and 217 | max(slo:sli_error:ratio_rate6h{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (6 * 0.06)) without (sloth_window) 218 | ) 219 | labels: 220 | category: availability 221 | sloth_severity: page 222 | type: slo 223 | annotations: 224 | summary: High error rate on 'webshop' endpoints requests responses 225 | title: (page) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget 226 | burn rate is too fast. 227 | - alert: Endpoints-HighErrorRate 228 | expr: | 229 | ( 230 | max(slo:sli_error:ratio_rate2h{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (3 * 0.06)) without (sloth_window) 231 | and 232 | max(slo:sli_error:ratio_rate1d{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (3 * 0.06)) without (sloth_window) 233 | ) 234 | or 235 | ( 236 | max(slo:sli_error:ratio_rate6h{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (1 * 0.06)) without (sloth_window) 237 | and 238 | max(slo:sli_error:ratio_rate3d{sloth_id="web-shop-endpoint-availability", sloth_service="web-shop", sloth_slo="endpoint-availability"} > (1 * 0.06)) without (sloth_window) 239 | ) 240 | labels: 241 | category: availability 242 | sloth_severity: ticket 243 | type: slo 244 | annotations: 245 | summary: High error rate on 'webshop' endpoints requests responses 246 | title: (ticket) {{$labels.sloth_service}} {{$labels.sloth_slo}} SLO error budget 247 | burn rate is too fast. 248 | -------------------------------------------------------------------------------- /shop-simulator/webshop-protocol-demo-food.js: -------------------------------------------------------------------------------- 1 | // Scenario: Scenario_1 (executor: ramping-vus) 2 | 3 | import { sleep, group } from 'k6' 4 | import http from 'k6/http' 5 | import { SharedArray } from 'k6/data'; 6 | import { vu } from 'k6/execution'; 7 | 8 | const users = new SharedArray('shop users', function() { 9 | return [ 10 | {"username": "Stefan", "cat": "Chicken"}, 11 | {"username": "Kris", "cat": "Schnitzel"}, 12 | {"username": "Raul", "cat": "Shrimps"}, 13 | {"username": "Andreas", "cat": "Salmon"}, 14 | {"username": "Willie", "cat": "Pasta"}, 15 | {"username": "Aengus", "cat": "Chicken"}, 16 | {"username": "Paul", "cat": "Shrimps"}, 17 | {"username": "Emil", "cat": "Salmon"}, 18 | {"username": "Kris", "cat": "Pasta"}, 19 | {"username": "Cyril", "cat": "Pasta"}, 20 | {"username": "Devin", "cat": "Shrimps"}, 21 | {"username": "Mattias", "cat": "Chicken"}, 22 | {"username": "Alain", "cat": "Shrimps"}, 23 | {"username": "Nabeel", "cat": "Salmon"}, 24 | {"username": "Kris", "cat": "Salmon"}, 25 | {"username": "Inge", "cat": "Shrimps"}, 26 | {"username": "Andreas", "cat": "Salmon"}, 27 | {"username": "Ivan", "cat": "Pasta"}, 28 | {"username": "Emil", "cat": "Shrimps"}, 29 | {"username": "Ward", "cat": "Chicken"}, 30 | {"username": "Hans", "cat": "Salmon"} 31 | ] 32 | }); 33 | 34 | const usersScenario2 = new SharedArray('other shop users', function(){ 35 | return [ 36 | "Ragnar", "Olaf", "Gustaf", "Sven", "Olof", "Roderik", "Bjorn", "Kalle", "Linda", "Olle", "Harald", "John", "Isidor", "Isildur", "Aragorn", "Legolas", "Gimli", "Gandalf", "Ed", "Bo" 37 | ] 38 | }) 39 | 40 | export const options = { 41 | ext: { 42 | loadimpact: { 43 | projectID: 3569409, 44 | distribution: { 45 | 'amazon:us:ashburn': { loadZone: 'amazon:us:ashburn', percent: 20 }, 46 | 'amazon:fr:paris': { loadZone: 'amazon:fr:paris', percent: 80 }, 47 | }, 48 | apm: [], 49 | name: "Webshop Test" 50 | }, 51 | }, 52 | thresholds: {}, 53 | scenarios: { 54 | Scenario_1: { 55 | executor: 'per-vu-iterations', 56 | gracefulStop: '30s', 57 | iterations: 1000, 58 | maxDuration: '2h30m', 59 | vus: 5, 60 | exec: 'scenario_1', 61 | }, 62 | Scenario_2: { 63 | executor: 'ramping-vus', 64 | startVUs: 1, 65 | stages: [ 66 | { duration: '1m', target: 5}, 67 | { duration: '1m', target: 2}, 68 | { duration: '1m', target: 5}, 69 | { duration: '10m',target: 2}, 70 | { duration: '1m', target: 5}, 71 | { duration: '10m', target: 2}, 72 | { duration: '10m', target: 5}, 73 | ], 74 | gracefulRampDown: '0s', 75 | exec: 'scenario_2', 76 | }, 77 | }, 78 | } 79 | 80 | export function scenario_1() { 81 | let response 82 | let user = users[Math.floor(Math.random() * users.length)]; 83 | user = users[1]; 84 | console.log(`http://${__ENV.HOSTNAME}:3389/shop?name=${user.username}`) 85 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${user.username}`, function () { 86 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${user.username}`, { 87 | headers: { 88 | 'upgrade-insecure-requests': '1', 89 | }, 90 | }) 91 | sleep(2.6) 92 | }) 93 | 94 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/shop?product=Chicken&name=${user.username}`, function () { 95 | response = http.post( 96 | `http://${__ENV.HOSTNAME}:3389/shop?product=Chicken&name=${user.username}`, 97 | { 98 | product: `Chicken`, 99 | }, 100 | { 101 | headers: { 102 | 'content-type': 'application/x-www-form-urlencoded', 103 | origin: 'http://${__ENV.HOSTNAME}:3389', 104 | 'upgrade-insecure-requests': '1', 105 | }, 106 | } 107 | ) 108 | sleep(2) 109 | }) 110 | 111 | group(`page_3 - http://${__ENV.HOSTNAME}:3389/shop?product=Carlos&name=${user.username}`, function () { 112 | response = http.post( 113 | `http://${__ENV.HOSTNAME}:3389/shop?product=${user.cat}&name=${user.username}`, 114 | { 115 | product: 'Carlos', 116 | }, 117 | { 118 | headers: { 119 | 'content-type': 'application/x-www-form-urlencoded', 120 | origin: 'http://${__ENV.HOSTNAME}:3389', 121 | 'upgrade-insecure-requests': '1', 122 | }, 123 | } 124 | ) 125 | sleep(2) 126 | }) 127 | 128 | group(`page_4 - http://${__ENV.HOSTNAME}:3389/shop?product=Pasta&name=${user.username}`, function () { 129 | response = http.post( 130 | `http://${__ENV.HOSTNAME}:3389/shop?product=Pasta&name=${user.username}`, 131 | { 132 | product: 'Pasta', 133 | }, 134 | { 135 | headers: { 136 | 'content-type': 'application/x-www-form-urlencoded', 137 | origin: 'http://${__ENV.HOSTNAME}:3389', 138 | 'upgrade-insecure-requests': '1', 139 | }, 140 | } 141 | ) 142 | sleep(2.3) 143 | }) 144 | 145 | group(`page_5 - http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}`, function () { 146 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}`, { 147 | headers: { 148 | 'upgrade-insecure-requests': '1', 149 | }, 150 | }) 151 | sleep(3.4) 152 | 153 | response = http.post( 154 | `http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}`, 155 | { 156 | name: `${user.username}`, 157 | }, 158 | { 159 | headers: { 160 | 'content-type': 'application/x-www-form-urlencoded', 161 | origin: 'http://${__ENV.HOSTNAME}:3389', 162 | 'upgrade-insecure-requests': '1', 163 | }, 164 | } 165 | ) 166 | sleep(3.1) 167 | }) 168 | 169 | group(`page_6 - http://${__ENV.HOSTNAME}:3389/shop?name=${user.username}#cats`, function () { 170 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${user.username}`, { 171 | headers: { 172 | 'upgrade-insecure-requests': '1', 173 | }, 174 | }) 175 | sleep(2) 176 | }) 177 | 178 | group(`page_7 - http://${__ENV.HOSTNAME}:3389/shop?product=Salmon&name=${user.username}`, function () { 179 | response = http.post( 180 | `http://${__ENV.HOSTNAME}:3389/shop?product=Salmon&name=${user.username}`, 181 | { 182 | product: 'Salmon', 183 | }, 184 | { 185 | headers: { 186 | 'content-type': 'application/x-www-form-urlencoded', 187 | origin: 'http://${__ENV.HOSTNAME}:3389', 188 | 'upgrade-insecure-requests': '1', 189 | }, 190 | } 191 | ) 192 | sleep(2) 193 | }) 194 | 195 | group(`page_8 - http://${__ENV.HOSTNAME}:3389/shop?product=Shrimps&name=${user.username}`, function () { 196 | response = http.post( 197 | `http://${__ENV.HOSTNAME}:3389/shop?product=Shrimps&name=${user.username}`, 198 | { 199 | product: 'Shrimps', 200 | }, 201 | { 202 | headers: { 203 | 'content-type': 'application/x-www-form-urlencoded', 204 | origin: 'http://${__ENV.HOSTNAME}:3389', 205 | 'upgrade-insecure-requests': '1', 206 | }, 207 | } 208 | ) 209 | sleep(2) 210 | }) 211 | 212 | group(`page_9 - http://${__ENV.HOSTNAME}:3389/shop?product=Pasta&name=${user.username}`, function () { 213 | response = http.post( 214 | `http://${__ENV.HOSTNAME}:3389/shop?product=Pasta&name=${user.username}`, 215 | { 216 | product: 'Pasta', 217 | }, 218 | { 219 | headers: { 220 | 'content-type': 'application/x-www-form-urlencoded', 221 | origin: 'http://${__ENV.HOSTNAME}:3389', 222 | 'upgrade-insecure-requests': '1', 223 | }, 224 | } 225 | ) 226 | sleep(2) 227 | }) 228 | 229 | group(`page_10 - http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}`, function () { 230 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}`, { 231 | headers: { 232 | 'upgrade-insecure-requests': '1', 233 | }, 234 | }) 235 | sleep(2.9) 236 | }) 237 | 238 | group(`page_11a - http://${__ENV.HOSTNAME}:3389/shop?product=Pasta&name=${user.username}`, function () { 239 | response = http.post( 240 | `http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}&discount=true`, 241 | { 242 | name: `${user.username}`, 243 | }, 244 | { 245 | headers: { 246 | 'content-type': 'application/x-www-form-urlencoded', 247 | origin: 'http://${__ENV.HOSTNAME}:3389', 248 | 'upgrade-insecure-requests': '1', 249 | }, 250 | } 251 | ) 252 | sleep(2) 253 | }) 254 | 255 | group(`page_11b - http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}&checkout=true`, function () { 256 | response = http.post( 257 | `http://${__ENV.HOSTNAME}:3389/cart?name=${user.username}&checkout=true`, 258 | { 259 | name: `${user.username}`, 260 | }, 261 | { 262 | headers: { 263 | 'content-type': 'application/x-www-form-urlencoded', 264 | origin: `http://${__ENV.HOSTNAME}:3389`, 265 | 'upgrade-insecure-requests': '1', 266 | }, 267 | } 268 | ) 269 | sleep(10) 270 | }) 271 | } 272 | 273 | export function scenario_2() { 274 | let response 275 | const randomUser = usersScenario2[Math.floor(Math.random() * usersScenario2.length)]; 276 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${randomUser}#cats`, function () { 277 | 278 | 279 | response = http.post( 280 | `http://${__ENV.HOSTNAME}:3389/shop?product=Salmon&name=${randomUser}`, 281 | { 282 | product: "Salmon" 283 | }, 284 | { 285 | headers: { 286 | 'content-type': 'application/x-www-form-urlencoded', 287 | origin: 'http://${__ENV.HOSTNAME}:3389', 288 | 'upgrade-insecure-requests': '1', 289 | }, 290 | 291 | 292 | } 293 | ) 294 | sleep(1) 295 | }) 296 | 297 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, function () { 298 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, { 299 | headers: { 300 | 'upgrade-insecure-requests': '1', 301 | accept: 302 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 303 | 'accept-encoding': 'gzip, deflate', 304 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 305 | }, 306 | }) 307 | sleep(0.6) 308 | 309 | response = http.post( 310 | `http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}&checkout=true`, 311 | '{"name":"${randomUser}"}', 312 | { 313 | headers: { 314 | 'content-type': 'application/json', 315 | accept: '*/*', 316 | origin: `http://${__ENV.HOSTNAME}:3389`, 317 | 'accept-encoding': 'gzip, deflate', 318 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 319 | }, 320 | } 321 | ) 322 | sleep(0.6) 323 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, { 324 | headers: { 325 | 'upgrade-insecure-requests': '1', 326 | accept: 327 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 328 | 'accept-encoding': 'gzip, deflate', 329 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 330 | }, 331 | }) 332 | sleep(0.6) 333 | }) 334 | 335 | } 336 | -------------------------------------------------------------------------------- /shop-simulator/webshop-protocol-demo.js: -------------------------------------------------------------------------------- 1 | // Scenario: Scenario_1 (executor: ramping-vus) 2 | 3 | import { sleep, group } from 'k6' 4 | import http from 'k6/http' 5 | import { SharedArray } from 'k6/data'; 6 | import { vu } from 'k6/execution'; 7 | 8 | const users = new SharedArray('shop users', function() { 9 | return [ 10 | {"username": "Stefan", "cat": "Meows"}, 11 | {"username": "Kris", "cat": "Thor"}, 12 | {"username": "Raul", "cat": "Charlie"}, 13 | {"username": "Andreas", "cat": "Loki"}, 14 | {"username": "Willie", "cat": "Carla"}, 15 | {"username": "Aengus", "cat": "Meows"}, 16 | {"username": "Paul", "cat": "Charlie"}, 17 | {"username": "Emil", "cat": "Loki"}, 18 | {"username": "Kris", "cat": "Carla"}, 19 | {"username": "Cyril", "cat": "Carla"}, 20 | {"username": "Devin", "cat": "Charlie"}, 21 | {"username": "Mattias", "cat": "Meows"}, 22 | {"username": "Alain", "cat": "Charlie"}, 23 | {"username": "Nabeel", "cat": "Loki"}, 24 | {"username": "Kris", "cat": "Loki"}, 25 | {"username": "Inge", "cat": "Charlie"}, 26 | {"username": "Andreas", "cat": "Loki"}, 27 | {"username": "Ivan", "cat": "Carla"}, 28 | {"username": "Emil", "cat": "Charlie"}, 29 | {"username": "Ward", "cat": "Meows"}, 30 | {"username": "Hans", "cat": "Loki"} 31 | ] 32 | }); 33 | 34 | const usersScenario2 = new SharedArray('other shop users', function(){ 35 | return [ 36 | "Ragnar", "Olaf", "Gustaf", "Sven", "Olof", "Roderik", "Bjorn", "Kalle", "Linda", "Olle", "Harald", "John", "Isidor", "Isildur", "Aragorn", "Legolas", "Gimli", "Gandalf", "Ed", "Bo" 37 | ] 38 | }) 39 | 40 | export const options = { 41 | ext: { 42 | loadimpact: { 43 | projectID: 3569409, 44 | distribution: { 45 | 'amazon:us:ashburn': { loadZone: 'amazon:us:ashburn', percent: 20 }, 46 | 'amazon:fr:paris': { loadZone: 'amazon:fr:paris', percent: 80 }, 47 | }, 48 | apm: [], 49 | name: "Webshop Test" 50 | }, 51 | }, 52 | thresholds: {}, 53 | scenarios: { 54 | Scenario_1: { 55 | executor: 'per-vu-iterations', 56 | gracefulStop: '30s', 57 | iterations: 1000, 58 | maxDuration: '2h30m', 59 | vus: users.length, 60 | exec: 'scenario_1', 61 | }, 62 | Scenario_2: { 63 | executor: 'ramping-vus', 64 | startVUs: 1, 65 | stages: [ 66 | { duration: '1m', target: 10}, 67 | { duration: '1m', target: 0}, 68 | { duration: '1m', target: 10}, 69 | { duration: '10m',target: 0}, 70 | { duration: '1m', target: 10}, 71 | { duration: '10m', target: 0}, 72 | { duration: '10m', target: 10}, 73 | ], 74 | gracefulRampDown: '0s', 75 | exec: 'scenario_2', 76 | }, 77 | }, 78 | } 79 | 80 | export function scenario_1() { 81 | let response 82 | 83 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, function () { 84 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, { 85 | headers: { 86 | 'upgrade-insecure-requests': '1', 87 | }, 88 | }) 89 | sleep(2.6) 90 | }) 91 | 92 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/shop?product=Meows&name=${users[vu.idInTest -1].username}`, function () { 93 | response = http.post( 94 | `http://${__ENV.HOSTNAME}:3389/shop?product=Meows&name=${users[vu.idInTest -1].username}`, 95 | { 96 | product: `Meows`, 97 | }, 98 | { 99 | headers: { 100 | 'content-type': 'application/x-www-form-urlencoded', 101 | origin: 'http://${__ENV.HOSTNAME}:3389', 102 | 'upgrade-insecure-requests': '1', 103 | }, 104 | } 105 | ) 106 | sleep(2) 107 | }) 108 | 109 | group(`page_3 - http://${__ENV.HOSTNAME}:3389/shop?product=Carlos&name=${users[vu.idInTest -1].username}`, function () { 110 | response = http.post( 111 | `http://${__ENV.HOSTNAME}:3389/shop?product=${users[vu.idInTest -1].cat}&name=${users[vu.idInTest -1].username}`, 112 | { 113 | product: 'Carlos', 114 | }, 115 | { 116 | headers: { 117 | 'content-type': 'application/x-www-form-urlencoded', 118 | origin: 'http://${__ENV.HOSTNAME}:3389', 119 | 'upgrade-insecure-requests': '1', 120 | }, 121 | } 122 | ) 123 | sleep(2) 124 | }) 125 | 126 | group(`page_4 - http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, function () { 127 | response = http.post( 128 | `http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, 129 | { 130 | product: 'Carla', 131 | }, 132 | { 133 | headers: { 134 | 'content-type': 'application/x-www-form-urlencoded', 135 | origin: 'http://${__ENV.HOSTNAME}:3389', 136 | 'upgrade-insecure-requests': '1', 137 | }, 138 | } 139 | ) 140 | sleep(2.3) 141 | }) 142 | 143 | group(`page_5 - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, function () { 144 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, { 145 | headers: { 146 | 'upgrade-insecure-requests': '1', 147 | }, 148 | }) 149 | sleep(3.4) 150 | 151 | response = http.post( 152 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, 153 | { 154 | name: `${users[vu.idInTest -1].username}`, 155 | }, 156 | { 157 | headers: { 158 | 'content-type': 'application/x-www-form-urlencoded', 159 | origin: 'http://${__ENV.HOSTNAME}:3389', 160 | 'upgrade-insecure-requests': '1', 161 | }, 162 | } 163 | ) 164 | sleep(3.1) 165 | }) 166 | 167 | group(`page_6 - http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}#cats`, function () { 168 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, { 169 | headers: { 170 | 'upgrade-insecure-requests': '1', 171 | }, 172 | }) 173 | sleep(2) 174 | }) 175 | 176 | group(`page_7 - http://${__ENV.HOSTNAME}:3389/shop?product=Loki&name=${users[vu.idInTest -1].username}`, function () { 177 | response = http.post( 178 | `http://${__ENV.HOSTNAME}:3389/shop?product=Loki&name=${users[vu.idInTest -1].username}`, 179 | { 180 | product: 'Loki', 181 | }, 182 | { 183 | headers: { 184 | 'content-type': 'application/x-www-form-urlencoded', 185 | origin: 'http://${__ENV.HOSTNAME}:3389', 186 | 'upgrade-insecure-requests': '1', 187 | }, 188 | } 189 | ) 190 | sleep(2) 191 | }) 192 | 193 | group(`page_8 - http://${__ENV.HOSTNAME}:3389/shop?product=Charlie&name=${users[vu.idInTest -1].username}`, function () { 194 | response = http.post( 195 | `http://${__ENV.HOSTNAME}:3389/shop?product=Charlie&name=${users[vu.idInTest -1].username}`, 196 | { 197 | product: 'Charlie', 198 | }, 199 | { 200 | headers: { 201 | 'content-type': 'application/x-www-form-urlencoded', 202 | origin: 'http://${__ENV.HOSTNAME}:3389', 203 | 'upgrade-insecure-requests': '1', 204 | }, 205 | } 206 | ) 207 | sleep(2) 208 | }) 209 | 210 | group(`page_9 - http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, function () { 211 | response = http.post( 212 | `http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, 213 | { 214 | product: 'Carla', 215 | }, 216 | { 217 | headers: { 218 | 'content-type': 'application/x-www-form-urlencoded', 219 | origin: 'http://${__ENV.HOSTNAME}:3389', 220 | 'upgrade-insecure-requests': '1', 221 | }, 222 | } 223 | ) 224 | sleep(2) 225 | }) 226 | 227 | group(`page_10 - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, function () { 228 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, { 229 | headers: { 230 | 'upgrade-insecure-requests': '1', 231 | }, 232 | }) 233 | sleep(2.9) 234 | }) 235 | 236 | group(`page_11a - http://${__ENV.HOSTNAME}:3389/shop?product=Carla&name=${users[vu.idInTest -1].username}`, function () { 237 | response = http.post( 238 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&discount=true`, 239 | { 240 | name: `${users[vu.idInTest -1].username}`, 241 | }, 242 | { 243 | headers: { 244 | 'content-type': 'application/x-www-form-urlencoded', 245 | origin: 'http://${__ENV.HOSTNAME}:3389', 246 | 'upgrade-insecure-requests': '1', 247 | }, 248 | } 249 | ) 250 | sleep(2) 251 | }) 252 | 253 | group(`page_11b - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&checkout=true`, function () { 254 | response = http.post( 255 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&checkout=true`, 256 | { 257 | name: `${users[vu.idInTest -1].username}`, 258 | }, 259 | { 260 | headers: { 261 | 'content-type': 'application/x-www-form-urlencoded', 262 | origin: `http://${__ENV.HOSTNAME}:3389`, 263 | 'upgrade-insecure-requests': '1', 264 | }, 265 | } 266 | ) 267 | sleep(10) 268 | }) 269 | } 270 | 271 | export function scenario_2() { 272 | let response 273 | const randomUser = usersScenario2[Math.floor(Math.random() * usersScenario2.length)]; 274 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${randomUser}#cats`, function () { 275 | 276 | 277 | response = http.post( 278 | `http://${__ENV.HOSTNAME}:3389/shop?product=Loki&name=${randomUser}`, 279 | { 280 | product: "Loki" 281 | }, 282 | { 283 | headers: { 284 | 'content-type': 'application/x-www-form-urlencoded', 285 | origin: 'http://${__ENV.HOSTNAME}:3389', 286 | 'upgrade-insecure-requests': '1', 287 | }, 288 | 289 | 290 | } 291 | ) 292 | sleep(1) 293 | }) 294 | 295 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, function () { 296 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, { 297 | headers: { 298 | 'upgrade-insecure-requests': '1', 299 | accept: 300 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 301 | 'accept-encoding': 'gzip, deflate', 302 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 303 | }, 304 | }) 305 | sleep(0.6) 306 | 307 | response = http.post( 308 | `http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}&checkout=true`, 309 | '{"name":"${randomUser}"}', 310 | { 311 | headers: { 312 | 'content-type': 'application/json', 313 | accept: '*/*', 314 | origin: `http://${__ENV.HOSTNAME}:3389`, 315 | 'accept-encoding': 'gzip, deflate', 316 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 317 | }, 318 | } 319 | ) 320 | sleep(0.6) 321 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, { 322 | headers: { 323 | 'upgrade-insecure-requests': '1', 324 | accept: 325 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 326 | 'accept-encoding': 'gzip, deflate', 327 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 328 | }, 329 | }) 330 | sleep(0.6) 331 | }) 332 | 333 | } -------------------------------------------------------------------------------- /shop-simulator/webshop-protocol-demo-phones.js: -------------------------------------------------------------------------------- 1 | // Scenario: Scenario_1 (executor: ramping-vus) 2 | 3 | import { sleep, group } from 'k6' 4 | import http from 'k6/http' 5 | import { SharedArray } from 'k6/data'; 6 | import { vu } from 'k6/execution'; 7 | 8 | const users = new SharedArray('shop users', function() { 9 | return [ 10 | {"username": "Stefan", "cat": "XIAOMI"}, 11 | {"username": "Kris", "cat": "GOOGLE"}, 12 | {"username": "Raul", "cat": "ZTE"}, 13 | {"username": "Andreas", "cat": "OPPO"}, 14 | {"username": "Willie", "cat": "SAMSUNG"}, 15 | {"username": "Aengus", "cat": "XIAOMI"}, 16 | {"username": "Paul", "cat": "ZTE"}, 17 | {"username": "Emil", "cat": "OPPO"}, 18 | {"username": "Kris", "cat": "SAMSUNG"}, 19 | {"username": "Cyril", "cat": "SAMSUNG"}, 20 | {"username": "Devin", "cat": "ZTE"}, 21 | {"username": "Mattias", "cat": "XIAOMI"}, 22 | {"username": "Alain", "cat": "ZTE"}, 23 | {"username": "Nabeel", "cat": "OPPO"}, 24 | {"username": "Kris", "cat": "OPPO"}, 25 | {"username": "Inge", "cat": "ZTE"}, 26 | {"username": "Andreas", "cat": "OPPO"}, 27 | {"username": "Ivan", "cat": "SAMSUNG"}, 28 | {"username": "Emil", "cat": "ZTE"}, 29 | {"username": "Ward", "cat": "XIAOMI"}, 30 | {"username": "Hans", "cat": "OPPO"} 31 | ] 32 | }); 33 | 34 | const usersScenario2 = new SharedArray('other shop users', function(){ 35 | return [ 36 | "Ragnar", "Olaf", "Gustaf", "Sven", "Olof", "Roderik", "Bjorn", "Kalle", "Linda", "Olle", "Harald", "John", "Isidor", "Isildur", "Aragorn", "Legolas", "Gimli", "Gandalf", "Ed", "Bo" 37 | ] 38 | }) 39 | 40 | export const options = { 41 | ext: { 42 | loadimpact: { 43 | projectID: 3569409, 44 | distribution: { 45 | 'amazon:us:ashburn': { loadZone: 'amazon:us:ashburn', percent: 20 }, 46 | 'amazon:fr:paris': { loadZone: 'amazon:fr:paris', percent: 80 }, 47 | }, 48 | apm: [], 49 | name: "Webshop Test" 50 | }, 51 | }, 52 | thresholds: {}, 53 | scenarios: { 54 | Scenario_1: { 55 | executor: 'per-vu-iterations', 56 | gracefulStop: '30s', 57 | iterations: 1000, 58 | maxDuration: '2h30m', 59 | vus: users.length, 60 | exec: 'scenario_1', 61 | }, 62 | Scenario_2: { 63 | executor: 'ramping-vus', 64 | startVUs: 1, 65 | stages: [ 66 | { duration: '1m', target: 10}, 67 | { duration: '1m', target: 0}, 68 | { duration: '1m', target: 10}, 69 | { duration: '10m',target: 0}, 70 | { duration: '1m', target: 10}, 71 | { duration: '10m', target: 0}, 72 | { duration: '10m', target: 10}, 73 | ], 74 | gracefulRampDown: '0s', 75 | exec: 'scenario_2', 76 | }, 77 | }, 78 | } 79 | 80 | export function scenario_1() { 81 | let response 82 | 83 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, function () { 84 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, { 85 | headers: { 86 | 'upgrade-insecure-requests': '1', 87 | }, 88 | }) 89 | sleep(2.6) 90 | }) 91 | 92 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/shop?product=XIAOMI&name=${users[vu.idInTest -1].username}`, function () { 93 | response = http.post( 94 | `http://${__ENV.HOSTNAME}:3389/shop?product=XIAOMI&name=${users[vu.idInTest -1].username}`, 95 | { 96 | product: `XIAOMI`, 97 | }, 98 | { 99 | headers: { 100 | 'content-type': 'application/x-www-form-urlencoded', 101 | origin: 'http://${__ENV.HOSTNAME}:3389', 102 | 'upgrade-insecure-requests': '1', 103 | }, 104 | } 105 | ) 106 | sleep(2) 107 | }) 108 | 109 | group(`page_3 - http://${__ENV.HOSTNAME}:3389/shop?product=Carlos&name=${users[vu.idInTest -1].username}`, function () { 110 | response = http.post( 111 | `http://${__ENV.HOSTNAME}:3389/shop?product=${users[vu.idInTest -1].cat}&name=${users[vu.idInTest -1].username}`, 112 | { 113 | product: 'Carlos', 114 | }, 115 | { 116 | headers: { 117 | 'content-type': 'application/x-www-form-urlencoded', 118 | origin: 'http://${__ENV.HOSTNAME}:3389', 119 | 'upgrade-insecure-requests': '1', 120 | }, 121 | } 122 | ) 123 | sleep(2) 124 | }) 125 | 126 | group(`page_4 - http://${__ENV.HOSTNAME}:3389/shop?product=SAMSUNG&name=${users[vu.idInTest -1].username}`, function () { 127 | response = http.post( 128 | `http://${__ENV.HOSTNAME}:3389/shop?product=SAMSUNG&name=${users[vu.idInTest -1].username}`, 129 | { 130 | product: 'SAMSUNG', 131 | }, 132 | { 133 | headers: { 134 | 'content-type': 'application/x-www-form-urlencoded', 135 | origin: 'http://${__ENV.HOSTNAME}:3389', 136 | 'upgrade-insecure-requests': '1', 137 | }, 138 | } 139 | ) 140 | sleep(2.3) 141 | }) 142 | 143 | group(`page_5 - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, function () { 144 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, { 145 | headers: { 146 | 'upgrade-insecure-requests': '1', 147 | }, 148 | }) 149 | sleep(3.4) 150 | 151 | response = http.post( 152 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, 153 | { 154 | name: `${users[vu.idInTest -1].username}`, 155 | }, 156 | { 157 | headers: { 158 | 'content-type': 'application/x-www-form-urlencoded', 159 | origin: 'http://${__ENV.HOSTNAME}:3389', 160 | 'upgrade-insecure-requests': '1', 161 | }, 162 | } 163 | ) 164 | sleep(3.1) 165 | }) 166 | 167 | group(`page_6 - http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}#cats`, function () { 168 | response = http.get(`http://${__ENV.HOSTNAME}:3389/shop?name=${users[vu.idInTest -1].username}`, { 169 | headers: { 170 | 'upgrade-insecure-requests': '1', 171 | }, 172 | }) 173 | sleep(2) 174 | }) 175 | 176 | group(`page_7 - http://${__ENV.HOSTNAME}:3389/shop?product=OPPO&name=${users[vu.idInTest -1].username}`, function () { 177 | response = http.post( 178 | `http://${__ENV.HOSTNAME}:3389/shop?product=OPPO&name=${users[vu.idInTest -1].username}`, 179 | { 180 | product: 'OPPO', 181 | }, 182 | { 183 | headers: { 184 | 'content-type': 'application/x-www-form-urlencoded', 185 | origin: 'http://${__ENV.HOSTNAME}:3389', 186 | 'upgrade-insecure-requests': '1', 187 | }, 188 | } 189 | ) 190 | sleep(2) 191 | }) 192 | 193 | group(`page_8 - http://${__ENV.HOSTNAME}:3389/shop?product=ZTE&name=${users[vu.idInTest -1].username}`, function () { 194 | response = http.post( 195 | `http://${__ENV.HOSTNAME}:3389/shop?product=ZTE&name=${users[vu.idInTest -1].username}`, 196 | { 197 | product: 'ZTE', 198 | }, 199 | { 200 | headers: { 201 | 'content-type': 'application/x-www-form-urlencoded', 202 | origin: 'http://${__ENV.HOSTNAME}:3389', 203 | 'upgrade-insecure-requests': '1', 204 | }, 205 | } 206 | ) 207 | sleep(2) 208 | }) 209 | 210 | group(`page_9 - http://${__ENV.HOSTNAME}:3389/shop?product=SAMSUNG&name=${users[vu.idInTest -1].username}`, function () { 211 | response = http.post( 212 | `http://${__ENV.HOSTNAME}:3389/shop?product=SAMSUNG&name=${users[vu.idInTest -1].username}`, 213 | { 214 | product: 'SAMSUNG', 215 | }, 216 | { 217 | headers: { 218 | 'content-type': 'application/x-www-form-urlencoded', 219 | origin: 'http://${__ENV.HOSTNAME}:3389', 220 | 'upgrade-insecure-requests': '1', 221 | }, 222 | } 223 | ) 224 | sleep(2) 225 | }) 226 | 227 | group(`page_10 - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, function () { 228 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}`, { 229 | headers: { 230 | 'upgrade-insecure-requests': '1', 231 | }, 232 | }) 233 | sleep(2.9) 234 | }) 235 | 236 | group(`page_11a - http://${__ENV.HOSTNAME}:3389/shop?product=SAMSUNG&name=${users[vu.idInTest -1].username}`, function () { 237 | response = http.post( 238 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&discount=true`, 239 | { 240 | name: `${users[vu.idInTest -1].username}`, 241 | }, 242 | { 243 | headers: { 244 | 'content-type': 'application/x-www-form-urlencoded', 245 | origin: 'http://${__ENV.HOSTNAME}:3389', 246 | 'upgrade-insecure-requests': '1', 247 | }, 248 | } 249 | ) 250 | sleep(2) 251 | }) 252 | 253 | group(`page_11b - http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&checkout=true`, function () { 254 | response = http.post( 255 | `http://${__ENV.HOSTNAME}:3389/cart?name=${users[vu.idInTest -1].username}&checkout=true`, 256 | { 257 | name: `${users[vu.idInTest -1].username}`, 258 | }, 259 | { 260 | headers: { 261 | 'content-type': 'application/x-www-form-urlencoded', 262 | origin: `http://${__ENV.HOSTNAME}:3389`, 263 | 'upgrade-insecure-requests': '1', 264 | }, 265 | } 266 | ) 267 | sleep(10) 268 | }) 269 | } 270 | 271 | export function scenario_2() { 272 | let response 273 | const randomUser = usersScenario2[Math.floor(Math.random() * usersScenario2.length)]; 274 | group(`page_1 - http://${__ENV.HOSTNAME}:3389/shop?name=${randomUser}#cats`, function () { 275 | 276 | 277 | response = http.post( 278 | `http://${__ENV.HOSTNAME}:3389/shop?product=OPPO&name=${randomUser}`, 279 | { 280 | product: "OPPO" 281 | }, 282 | { 283 | headers: { 284 | 'content-type': 'application/x-www-form-urlencoded', 285 | origin: 'http://${__ENV.HOSTNAME}:3389', 286 | 'upgrade-insecure-requests': '1', 287 | }, 288 | 289 | 290 | } 291 | ) 292 | sleep(1) 293 | }) 294 | 295 | group(`page_2 - http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, function () { 296 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, { 297 | headers: { 298 | 'upgrade-insecure-requests': '1', 299 | accept: 300 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 301 | 'accept-encoding': 'gzip, deflate', 302 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 303 | }, 304 | }) 305 | sleep(0.6) 306 | 307 | response = http.post( 308 | `http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}&checkout=true`, 309 | '{"name":"${randomUser}"}', 310 | { 311 | headers: { 312 | 'content-type': 'application/json', 313 | accept: '*/*', 314 | origin: `http://${__ENV.HOSTNAME}:3389`, 315 | 'accept-encoding': 'gzip, deflate', 316 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 317 | }, 318 | } 319 | ) 320 | sleep(0.6) 321 | response = http.get(`http://${__ENV.HOSTNAME}:3389/cart?name=${randomUser}`, { 322 | headers: { 323 | 'upgrade-insecure-requests': '1', 324 | accept: 325 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 326 | 'accept-encoding': 'gzip, deflate', 327 | 'accept-language': 'en-US,en;q=0.9,de-AT;q=0.8,de;q=0.7', 328 | }, 329 | }) 330 | sleep(0.6) 331 | }) 332 | 333 | } 334 | --------------------------------------------------------------------------------