diff options
Diffstat (limited to 'src/load-generator')
| -rw-r--r-- | src/load-generator/Dockerfile | 22 | ||||
| -rw-r--r-- | src/load-generator/README.md | 13 | ||||
| -rw-r--r-- | src/load-generator/locustfile.py | 266 | ||||
| -rw-r--r-- | src/load-generator/people.json | 155 | ||||
| -rw-r--r-- | src/load-generator/requirements.txt | 14 |
5 files changed, 470 insertions, 0 deletions
diff --git a/src/load-generator/Dockerfile b/src/load-generator/Dockerfile new file mode 100644 index 0000000..45e5dcb --- /dev/null +++ b/src/load-generator/Dockerfile @@ -0,0 +1,22 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + + +FROM python:3.12-slim-bookworm AS base + +FROM base AS builder +RUN apt-get -qq update \ + && apt-get install -y --no-install-recommends g++ \ + && rm -rf /var/lib/apt/lists/* + +COPY ./src/load-generator/requirements.txt . +RUN pip install --prefix="/reqs" -r requirements.txt + +FROM base +WORKDIR /usr/src/app/ +COPY --from=builder /reqs /usr/local +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/pw-browsers +RUN playwright install --with-deps chromium +COPY ./src/load-generator/locustfile.py . +COPY ./src/load-generator/people.json . +ENTRYPOINT ["locust", "--skip-log-setup"] diff --git a/src/load-generator/README.md b/src/load-generator/README.md new file mode 100644 index 0000000..d28dc48 --- /dev/null +++ b/src/load-generator/README.md @@ -0,0 +1,13 @@ +# Load Generator + +The load generator creates simulated traffic to the demo. + +## Accessing the Load Generator + +You can access the web interface to Locust at `http://localhost:8080/loadgen/`. + +## Modifying the Load Generator + +Please see the [Locust +documentation](https://docs.locust.io/en/2.16.0/writing-a-locustfile.html) to +learn more about modifying the locustfile. diff --git a/src/load-generator/locustfile.py b/src/load-generator/locustfile.py new file mode 100644 index 0000000..c1ec95b --- /dev/null +++ b/src/load-generator/locustfile.py @@ -0,0 +1,266 @@ +#!/usr/bin/python + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import json +import os +import random +import uuid +import logging + +from locust import HttpUser, task, between +from locust_plugins.users.playwright import PlaywrightUser, pw, PageWithRetry, event + +from opentelemetry import context, baggage, trace +from opentelemetry.context import Context +from opentelemetry.metrics import set_meter_provider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.jinja2 import Jinja2Instrumentor +from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.instrumentation.system_metrics import SystemMetricsInstrumentor +from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor +from opentelemetry.instrumentation.logging import LoggingInstrumentor +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.resources import Resource + +from openfeature import api +from openfeature.contrib.provider.ofrep import OFREPProvider +from openfeature.contrib.hook.opentelemetry import TracingHook + +from playwright.async_api import Route, Request + +# Configure tracer provider first (needed for trace context in logs) +tracer_provider = TracerProvider() +trace.set_tracer_provider(tracer_provider) +tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(insecure=True))) + +# Configure logger provider with the same resource +logger_provider = LoggerProvider() +set_logger_provider(logger_provider) + +# Set up log exporter and processor +log_exporter = OTLPLogExporter(insecure=True) +logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter)) + +# Create logging handler that will include trace context +handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider) + +# Configure root logger +root_logger = logging.getLogger() +root_logger.addHandler(handler) +root_logger.setLevel(logging.INFO) + +# Configure metrics +metric_exporter = OTLPMetricExporter(insecure=True) +set_meter_provider(MeterProvider([PeriodicExportingMetricReader(metric_exporter)])) + +# Instrument logging to automatically inject trace context +LoggingInstrumentor().instrument(set_logging_format=True) + +# Instrumenting manually to avoid error with locust gevent monkey +Jinja2Instrumentor().instrument() +RequestsInstrumentor().instrument() +SystemMetricsInstrumentor().instrument() +URLLib3Instrumentor().instrument() + +logging.info("Instrumentation complete - logs will now include trace context") + +# Initialize Flagd provider +base_url = f"http://{os.environ.get('FLAGD_HOST', 'localhost')}:{os.environ.get('FLAGD_OFREP_PORT', 8016)}" +api.set_provider(OFREPProvider(base_url=base_url)) +api.add_hooks([TracingHook()]) + +def get_flagd_value(FlagName): + # Initialize OpenFeature + client = api.get_client() + return client.get_integer_value(FlagName, 0) + +categories = [ + "binoculars", + "telescopes", + "accessories", + "assembly", + "travel", + "books", + None, +] + +products = [ + "0PUK6V6EV0", + "1YMWWN1N4O", + "2ZYFJ3GM2N", + "66VCHSJNUP", + "6E92ZMYYFZ", + "9SIQT8TOJO", + "L9ECAV7KIM", + "LS4PSXUNUM", + "OLJCESPC7Z", + "HQTGWGPNH4", +] + +people_file = open('people.json') +people = json.load(people_file) + +class WebsiteUser(HttpUser): + wait_time = between(1, 10) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tracer = trace.get_tracer(__name__) + + @task(1) + def index(self): + with self.tracer.start_as_current_span("user_index", context=Context()): + logging.info("User accessing index page") + self.client.get("/") + + @task(10) + def browse_product(self): + product = random.choice(products) + with self.tracer.start_as_current_span("user_browse_product", context=Context(), attributes={"product.id": product}): + logging.info(f"User browsing product: {product}") + self.client.get("/api/products/" + product) + + @task(3) + def get_recommendations(self): + product = random.choice(products) + with self.tracer.start_as_current_span("user_get_recommendations", context=Context(), attributes={"product.id": product}): + logging.info(f"User getting recommendations for product: {product}") + params = { + "productIds": [product], + } + self.client.get("/api/recommendations", params=params) + + @task(3) + def get_ads(self): + category = random.choice(categories) + with self.tracer.start_as_current_span("user_get_ads", context=Context(), attributes={"category": str(category)}): + logging.info(f"User getting ads for category: {category}") + params = { + "contextKeys": [category], + } + self.client.get("/api/data/", params=params) + + @task(3) + def view_cart(self): + with self.tracer.start_as_current_span("user_view_cart", context=Context()): + logging.info("User viewing cart") + self.client.get("/api/cart") + + @task(2) + def add_to_cart(self, user=""): + if user == "": + user = str(uuid.uuid1()) + product = random.choice(products) + quantity = random.choice([1, 2, 3, 4, 5, 10]) + with self.tracer.start_as_current_span("user_add_to_cart", context=Context(), attributes={"user.id": user, "product.id": product, "quantity": quantity}): + logging.info(f"User {user} adding {quantity} of product {product} to cart") + self.client.get("/api/products/" + product) + cart_item = { + "item": { + "productId": product, + "quantity": quantity, + }, + "userId": user, + } + self.client.post("/api/cart", json=cart_item) + + @task(1) + def checkout(self): + user = str(uuid.uuid1()) + with self.tracer.start_as_current_span("user_checkout_single", context=Context(), attributes={"user.id": user}): + self.add_to_cart(user=user) + checkout_person = random.choice(people) + checkout_person["userId"] = user + self.client.post("/api/checkout", json=checkout_person) + logging.info(f"Checkout completed for user {user}") + + @task(1) + def checkout_multi(self): + user = str(uuid.uuid1()) + item_count = random.choice([2, 3, 4]) + with self.tracer.start_as_current_span("user_checkout_multi", context=Context(), + attributes={"user.id": user, "item.count": item_count}): + for i in range(item_count): + self.add_to_cart(user=user) + checkout_person = random.choice(people) + checkout_person["userId"] = user + self.client.post("/api/checkout", json=checkout_person) + logging.info(f"Multi-item checkout completed for user {user}") + + @task(5) + def flood_home(self): + flood_count = get_flagd_value("loadGeneratorFloodHomepage") + if flood_count > 0: + with self.tracer.start_as_current_span("user_flood_home", context=Context(), attributes={"flood.count": flood_count}): + logging.info(f"User flooding homepage {flood_count} times") + for _ in range(0, flood_count): + self.client.get("/") + + def on_start(self): + with self.tracer.start_as_current_span("user_session_start", context=Context()): + session_id = str(uuid.uuid4()) + logging.info(f"Starting user session: {session_id}") + ctx = baggage.set_baggage("session.id", session_id) + ctx = baggage.set_baggage("synthetic_request", "true", context=ctx) + context.attach(ctx) + self.index() + + +browser_traffic_enabled = os.environ.get("LOCUST_BROWSER_TRAFFIC_ENABLED", "").lower() in ("true", "yes", "on") + +if browser_traffic_enabled: + class WebsiteBrowserUser(PlaywrightUser): + headless = True # to use a headless browser, without a GUI + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.tracer = trace.get_tracer(__name__) + + @task + @pw + async def open_cart_page_and_change_currency(self, page: PageWithRetry): + with self.tracer.start_as_current_span("browser_change_currency", context=Context()): + try: + page.on("console", lambda msg: print(msg.text)) + await page.route('**/*', add_baggage_header) + await page.goto("/cart", wait_until="domcontentloaded") + await page.select_option('[name="currency_code"]', 'CHF') + await page.wait_for_timeout(2000) # giving the browser time to export the traces + logging.info("Currency changed to CHF") + except Exception as e: + logging.error(f"Error in change currency task: {str(e)}") + + @task + @pw + async def add_product_to_cart(self, page: PageWithRetry): + with self.tracer.start_as_current_span("browser_add_to_cart", context=Context()): + try: + page.on("console", lambda msg: print(msg.text)) + await page.route('**/*', add_baggage_header) + await page.goto("/", wait_until="domcontentloaded") + await page.click('p:has-text("Roof Binoculars")') + await page.wait_for_load_state("domcontentloaded") + await page.click('button:has-text("Add To Cart")') + await page.wait_for_load_state("domcontentloaded") + await page.wait_for_timeout(2000) # giving the browser time to export the traces + logging.info("Product added to cart successfully") + except Exception as e: + logging.error(f"Error in add to cart task: {str(e)}") + +async def add_baggage_header(route: Route, request: Request): + existing_baggage = request.headers.get('baggage', '') + headers = { + **request.headers, + 'baggage': ', '.join(filter(None, (existing_baggage, 'synthetic_request=true'))) + } + await route.continue_(headers=headers) diff --git a/src/load-generator/people.json b/src/load-generator/people.json new file mode 100644 index 0000000..5e04a93 --- /dev/null +++ b/src/load-generator/people.json @@ -0,0 +1,155 @@ +[ + { + "email": "larry_sergei@example.com", + "address": { + "streetAddress": "1600 Amphitheatre Parkway", + "zipCode": "94043", + "city": "Mountain View", + "state": "CA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4432-8015-6152-0454", + "creditCardExpirationMonth": 1, + "creditCardExpirationYear": 2039, + "creditCardCvv": 672 + } + }, + { + "email": "bill@example.com", + "address": { + "streetAddress": "One Microsoft Way", + "zipCode": "98052", + "city": "Redmond", + "state": "WA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4532-4211-7434-1278", + "creditCardExpirationMonth": 2, + "creditCardExpirationYear": 2039, + "creditCardCvv": 114 + } + }, + { + "email": "steve@example.com", + "address": { + "streetAddress": "One Apple Park Way", + "zipCode": "95014", + "city": "Cupertino", + "state": "CA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4532-6178-2799-1951", + "creditCardExpirationMonth": 3, + "creditCardExpirationYear": 2039, + "creditCardCvv": 239 + } + }, + { + "email": "mark@example.com", + "address": { + "streetAddress": "1 Hacker Way", + "zipCode": "94025", + "city": "Menlo Park", + "state": "CA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4539-1103-5661-7083", + "creditCardExpirationMonth": 4, + "creditCardExpirationYear": 2039, + "creditCardCvv": 784 + } + }, + { + "email": "jeff@example.com", + "address": { + "streetAddress": "410 Terry Ave N", + "zipCode": "98109", + "city": "Seattle", + "state": "WA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4916-0816-6217-7968", + "creditCardExpirationMonth": 5, + "creditCardExpirationYear": 2039, + "creditCardCvv": 397 + } + }, + { + "email": "reed@example.com", + "address": { + "streetAddress": "100 Winchester Circle", + "zipCode": "95032", + "city": "Los Gatos", + "state": "CA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4929-5431-0337-5647", + "creditCardExpirationMonth": 6, + "creditCardExpirationYear": 2039, + "creditCardCvv": 793 + } + }, + { + "email": "tobias@example.com", + "address": { + "streetAddress": "150 Elgin St", + "zipCode": "K2P1L4", + "city": "Ottawa", + "state": "ON", + "country": "Canada" + }, + "userCurrency": "CAD", + "creditCard": { + "creditCardNumber": "4763-1844-9699-8031", + "creditCardExpirationMonth": 7, + "creditCardExpirationYear": 2039, + "creditCardCvv": 488 + } + }, + { + "email": "jack@example.com", + "address": { + "streetAddress": "1355 Market St", + "zipCode": "94103", + "city": "San Francisco", + "state": "CA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4929-6495-8333-3657", + "creditCardExpirationMonth": 8, + "creditCardExpirationYear": 2039, + "creditCardCvv": 159 + } + }, + { + "email": "moore@example.com", + "address": { + "streetAddress": "2200 Mission College Blvd", + "zipCode": "95054", + "city": "Santa Clara", + "state": "CA", + "country": "United States" + }, + "userCurrency": "USD", + "creditCard": { + "creditCardNumber": "4485-4803-8707-3547", + "creditCardExpirationMonth": 9, + "creditCardExpirationYear": 2039, + "creditCardCvv": 682 + } + } +]
\ No newline at end of file diff --git a/src/load-generator/requirements.txt b/src/load-generator/requirements.txt new file mode 100644 index 0000000..1006354 --- /dev/null +++ b/src/load-generator/requirements.txt @@ -0,0 +1,14 @@ +Flask==3.1.2 +locust-plugins[playwright]==4.7.0 +python-json-logger==3.3.0 +openfeature-provider-ofrep==0.1.1 +openfeature-hooks-opentelemetry==0.2.0 +opentelemetry-api==1.37.0 +opentelemetry-exporter-otlp-proto-grpc==1.37.0 +opentelemetry-instrumentation-jinja2==0.58b0 +opentelemetry-instrumentation-requests==0.58b0 +opentelemetry-instrumentation-system-metrics==0.58b0 +opentelemetry-instrumentation-urllib3==0.58b0 +opentelemetry-sdk==1.37.0 +opentelemetry-semantic-conventions==0.58b0 +opentelemetry-instrumentation-logging==0.58b0 |
