diff --git a/pipeline/compose.yaml b/pipeline/compose.yaml new file mode 100644 index 0000000..9830bcc --- /dev/null +++ b/pipeline/compose.yaml @@ -0,0 +1,43 @@ +services: + # Video Voter Application + video-voter: + build: ../server + depends_on: + - otel-collector # Depends on otel-collector to be running first + environment: + OTEL_EXPORTER_OTLP_ENDPOINT: otel-collector:4317 + ports: + - "5000:5000" + + # OpenTelemetry Collector + otel-collector: + image: otel/opentelemetry-collector:0.100.0 # Use `otel/opentelemetry-collector-contrib` to support more backends. + command: + - "--config=/conf/otel-collector-config.yaml" # Autodiscovery folder changes between versions. Better to provide explicitly. + volumes: + - ./conf/otel-collector-config.yaml:/conf/otel-collector-config.yaml + ports: + - 8888:8888 # Prometheus metrics exposed by the collector + - 8889:8889 # Prometheus exporter metrics + - 13133:13133 # health_check extension + - 4317:4317 # OTLP gRPC receiver + - 4318:4318 # OTLP http receiver + restart: unless-stopped + + # Prometheus + prometheus: + image: prom/prometheus:latest + volumes: + - ./conf/prometheus-config.yaml:/etc/prometheus/prometheus.yml + depends_on: + - otel-collector + ports: + - "9090:9090" + + # Grafana + grafana: + image: grafana/grafana:latest + volumes: + - ./conf/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml + ports: + - "3000:3000" \ No newline at end of file diff --git a/pipeline/conf/grafana-datasources.yaml b/pipeline/conf/grafana-datasources.yaml new file mode 100644 index 0000000..3d8fcb3 --- /dev/null +++ b/pipeline/conf/grafana-datasources.yaml @@ -0,0 +1,10 @@ +datasources: + - name: Prometheus + type: prometheus + uid: prometheus-1 + url: http://prometheus:9090 + access: server + +# deleteDatasources: +# - name: Prometheus +# uid: prometheus-1 \ No newline at end of file diff --git a/pipeline/conf/otel-collector-config.yaml b/pipeline/conf/otel-collector-config.yaml new file mode 100644 index 0000000..b0dc85f --- /dev/null +++ b/pipeline/conf/otel-collector-config.yaml @@ -0,0 +1,27 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +processors: + batch: + +exporters: + prometheus: + endpoint: 0.0.0.0:8889 + namespace: default + debug: + verbosity: detailed + +extensions: + health_check: + +service: + extensions: [health_check] + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] + diff --git a/pipeline/conf/prometheus-config.yaml b/pipeline/conf/prometheus-config.yaml new file mode 100644 index 0000000..3d1132a --- /dev/null +++ b/pipeline/conf/prometheus-config.yaml @@ -0,0 +1,11 @@ +scrape_configs: + - job_name: "otel-collector-self-reporting" + scrape_interval: 15s + static_configs: + - targets: + - "otel-collector:8888" + - job_name: "otel-collector-exporter" + scrape_interval: 15s + static_configs: + - targets: + - "otel-collector:8889" diff --git a/server/app.py b/server/app.py index 9e372d9..256d9c3 100644 --- a/server/app.py +++ b/server/app.py @@ -1,9 +1,18 @@ +from apps.videos import videos +from apps.instrumentation import instrumentation from flask import Flask, request, render_template +from time import time + +from opentelemetry.metrics import get_meter_provider -from apps.videos import videos app = Flask(__name__) +default_meter = get_meter_provider().get_meter("default") +meter_likes = default_meter.create_counter("video_likes", description="Calls to the Like Endpoint") +meter_dislikes = default_meter.create_counter("video_dislikes", description="Calls to the Dislike Endpoint") +meter_latency_get = default_meter.create_histogram("get_video_latency_milliseconds", unit="ms", description="Latency of Video information retrieval") + @app.get("/api/v1/video") def get_videos(): @@ -14,21 +23,43 @@ def get_videos(): @app.get("/api/v1/video/") def get_video_details(id): - return videos.get(id) # Unhandled exception on purpose + start = time() + video = videos.get(id) + end = time() + meter_latency_get.record( + (end - start) * 1000, + { + "id": id, + }, + ) + + return video @app.get("/api/v1/video//like") def like_video(id): + meter_likes.add( + 1, + { + "id": id, + }, + ) videos.like(id) - return ("OK", 204) + return ("OK", 200) @app.get("/api/v1/video//dislike") def dislike_video(id): + meter_dislikes.add( + 1, + { + "id": id, + }, + ) videos.dislike(id) - return ("OK", 204) + return ("OK", 200) @app.route("/") @@ -47,5 +78,5 @@ def index(): if __name__ == "__main__": - # TODO: init Open Telemetry + instrumentation.init() app.run(host="0.0.0.0") diff --git a/server/apps/instrumentation/__init__.py b/server/apps/instrumentation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/apps/instrumentation/instrumentation.py b/server/apps/instrumentation/instrumentation.py new file mode 100644 index 0000000..60d7f58 --- /dev/null +++ b/server/apps/instrumentation/instrumentation.py @@ -0,0 +1,23 @@ +from opentelemetry import metrics +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + ConsoleMetricExporter, # Useful for debugging - exports the metrics to console + PeriodicExportingMetricReader, # Reader that batches metrics in configurable time intervals before sending it to the exporter +) + + +def init(): + # Configure the provider with the exporter and reader + exporter = OTLPMetricExporter( + insecure=True + ) # Endpoint provided via Environment variable - OTEL_EXPORTER_OTLP_ENDPOINT + reader = PeriodicExportingMetricReader( + exporter, export_interval_millis=15000, export_timeout_millis=5000 + ) + provider = MeterProvider(metric_readers=[reader]) + + # Set the global meter provider, and create a Meter for usage + metrics.set_meter_provider(provider) + + print("OTEL Metrics successfully initialised") diff --git a/server/apps/videos/videos.py b/server/apps/videos/videos.py index e209548..57c257d 100644 --- a/server/apps/videos/videos.py +++ b/server/apps/videos/videos.py @@ -28,6 +28,7 @@ def get(id): with open(DATABASE_JSON_LOCATION) as db: videos = json.load(db) + time.sleep(math.fabs(random.gauss(mu=0, sigma=0.5))) for video in videos: if video["id"] == id: return video @@ -42,7 +43,6 @@ def like(id): if video["id"] == id: video["likes"] += 1 continue - time.sleep(math.fabs(random.gauss(mu=1, sigma=0.5))) with open(DATABASE_JSON_LOCATION, "w") as db: json.dump(videos, db, indent=2) diff --git a/server/requirements.txt b/server/requirements.txt index fd2389c..ab59281 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -1,9 +1,21 @@ blinker==1.8.2 click==8.1.7 -flask==3.0.3 -importlib-metadata==7.1.0 +Deprecated==1.2.14 +Flask==3.0.3 +googleapis-common-protos==1.63.0 +grpcio==1.64.0 +importlib-metadata==7.0.0 itsdangerous==2.2.0 -jinja2==3.1.4 +Jinja2==3.1.4 MarkupSafe==2.1.5 -werkzeug==3.0.3 +opentelemetry-api==1.24.0 +opentelemetry-exporter-otlp-proto-common==1.24.0 +opentelemetry-exporter-otlp-proto-grpc==1.24.0 +opentelemetry-proto==1.24.0 +opentelemetry-sdk==1.24.0 +opentelemetry-semantic-conventions==0.45b0 +protobuf==4.25.3 +typing_extensions==4.11.0 +Werkzeug==3.0.3 +wrapt==1.16.0 zipp==3.18.2