Browse Source

Merge branch 'master' of github.com:nmasse-itix/www.itix.fr

master
Nicolas Massé 1 year ago
parent
commit
3b61592835
  1. 3
      .gitattributes
  2. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png
  3. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png
  4. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png
  5. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png
  6. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png
  7. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png
  8. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png
  9. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png
  10. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png
  11. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png
  12. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png
  13. 491
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md
  14. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif
  15. 452
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md
  16. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png
  17. 1
      content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg
  18. BIN
      content/english/blog/build-multi-architecture-container-images-with-kubernetes-buildah-tekton-and-qemu/quay-repository.png
  19. BIN
      content/english/blog/build-multi-architecture-container-images-with-kubernetes-buildah-tekton-and-qemu/quay-robot-account.png
  20. 7
      content/english/blog/build-multi-architecture-container-images-with-kubernetes-buildah-tekton-aws/index.md
  21. BIN
      content/english/blog/configure-redhat-sso-3scale-cli/openid-connect-issuer.png
  22. BIN
      content/english/blog/consistent-dns-name-resolution-for-virtual-machines-and-containers/consistent-dns-resolution-in-vm-and-containers.png
  23. BIN
      content/english/blog/deploy-openshift-single-node-in-kvm/assisted-installer-1.png
  24. BIN
      content/english/blog/deploy-openshift-single-node-in-kvm/assisted-installer-2.png
  25. BIN
      content/english/blog/deploy-openshift-single-node-in-kvm/assisted-installer-5.png
  26. BIN
      content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/k6-grafana-dashboard.png
  27. BIN
      content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/k6-simple-test.png
  28. BIN
      content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/k6-simple-test2.png
  29. BIN
      content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/prometheus-targets.png
  30. BIN
      content/english/blog/jmeter-assess-software-performances/control1.png
  31. BIN
      content/english/blog/jmeter-assess-software-performances/control2.png
  32. BIN
      content/english/blog/jmeter-assess-software-performances/control3.png
  33. BIN
      content/english/blog/jmeter-assess-software-performances/if.png
  34. BIN
      content/english/blog/jmeter-assess-software-performances/thread-group.png
  35. BIN
      content/english/blog/jmeter-assess-software-performances/udv.png
  36. BIN
      content/english/blog/nginx-with-tls-on-openwrt/make-menuconfig.png
  37. BIN
      content/english/blog/use-google-account-openid-connect-provider/auth-ok.png
  38. BIN
      content/english/blog/use-google-account-openid-connect-provider/auth.png
  39. BIN
      content/english/blog/use-google-account-openid-connect-provider/authorized-domains.png
  40. BIN
      content/english/blog/use-google-account-openid-connect-provider/create-credentials.png
  41. BIN
      content/english/blog/use-google-account-openid-connect-provider/create-project.png
  42. BIN
      content/english/blog/use-google-account-openid-connect-provider/links.png
  43. BIN
      content/english/blog/use-google-account-openid-connect-provider/oauth-consent.png
  44. BIN
      content/english/blog/use-google-account-openid-connect-provider/project-name.png
  45. BIN
      content/english/blog/use-google-account-openid-connect-provider/redirect-uri.png
  46. BIN
      content/english/blog/use-google-account-openid-connect-provider/script-start.png
  47. BIN
      content/english/blog/use-google-account-openid-connect-provider/script-url.png
  48. BIN
      content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/github-add-webhook.png
  49. BIN
      content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/github-webhook.png
  50. BIN
      content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/openshift-buildconfig-webhook.png
  51. BIN
      content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/openshift-rebuild.png
  52. BIN
      content/english/blog/writing-workshop-instructions-with-hugo/hugo-screenshot.png
  53. BIN
      content/english/speaking/apidays-2017/tweet-RedHatFrance.png
  54. BIN
      content/english/speaking/apidays-2017/tweet-jylls35.png
  55. BIN
      content/english/speaking/devoxx-france-2019/tweet-PetitMel_issa.png
  56. BIN
      content/english/speaking/devoxx-france-2019/tweet-cgodard.png
  57. BIN
      content/english/speaking/devoxx-france-2019/tweet-gbloquel.png
  58. BIN
      content/english/speaking/devoxx-france-2019/tweet-lbroudoux.png
  59. BIN
      content/english/speaking/devoxx-france-2019/tweet-sebi2706.png
  60. BIN
      content/english/speaking/red-hat-forum-2017/tweet-ylebihan.png
  61. 156
      content/english/speaking/red-hat-summit-connect-france-2024/index.md
  62. 3
      content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-ai.png
  63. 1
      content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-demo.mp4
  64. 3
      content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-hardware-architecture.png
  65. 3
      content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-plot.png
  66. 3
      content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-scenario.png
  67. 1
      content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-software-architecture.png
  68. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-clusters.png
  69. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-microservices.png
  70. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-namespaces.png
  71. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage-2.jpeg
  72. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage.jpeg
  73. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-testimonial.jpeg
  74. 1
      content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-winners.jpeg
  75. 1
      content/english/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-2.jpeg
  76. 1
      content/english/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-3.jpeg
  77. 1
      content/english/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo.jpeg
  78. BIN
      content/english/speaking/red-hat-tech-exchange-2019/2019-10-21-RHTE-Award.png
  79. BIN
      content/english/speaking/techweek-sg-2019/tweet-YadaYac.png
  80. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png
  81. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png
  82. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png
  83. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png
  84. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png
  85. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png
  86. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png
  87. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png
  88. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png
  89. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png
  90. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png
  91. 493
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md
  92. BIN
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif
  93. 91
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-no-time.m
  94. 108
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-with-time.m
  95. 65
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/exercise-validation.m
  96. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-query.png
  97. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-transform.png
  98. 3
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/leaderboard-simulation.mkv
  99. 6
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/stack.sh
  100. 12
      content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/to-gif.sh

3
.gitattributes

@ -3,3 +3,6 @@
*.jpg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.rnote filter=lfs diff=lfs merge=lfs -text *.rnote filter=lfs diff=lfs merge=lfs -text
*.xcf filter=lfs diff=lfs merge=lfs -text *.xcf filter=lfs diff=lfs merge=lfs -text
*.mkv filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png

491
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md

@ -0,0 +1,491 @@
---
title: "Behind the scenes at Open Code Quest: how I designed the Leaderboard"
date: 2024-11-05T00:00:00+02:00
#lastMod: 2024-10-11T00:00:00+02:00
opensource:
- Prometheus
- Grafana
topics:
- Observability
# Featured images for Social Media promotion (sorted from by priority)
images:
- counting-scheme-with-time.png
resources:
- '*.png'
- '*.svg'
- '*.gif'
---
At the {{< internalLink path="/speaking/red-hat-summit-connect-france-2024/index.md" >}}, I led a workshop for developers entitled "**Open Code Quest**".
In this workshop, developers had to code microservices using Quarkus, OpenShift and an Artificial Intelligence service: IBM's Granite model.
The workshop was designed as a speed competition: the first to complete all three exercises received a reward.
I designed and developed the **Leaderboard** which displays the progress of the participants and ranks them according to their speed.
Was it easy?
Well, not really, because I imposed a certain style on myself: using **Prometheus** and **Grafana**.
Follow me behind the scenes of Open Code Quest: how I designed the Leaderboard!
<!--more-->
## Workshop description
The **Open Code Quest** workshop has been designed to accommodate 96 participants who have to complete **and validate** 3 exercises.
Validating the successful completion of an exercise does not involve reading the participant's code: if the microservice starts and responds to requests, it is validated!
So there's no creative dimension, it's a race of speed and attention (you just have to read [the statement](https://cescoffier.github.io/quarkus-openshift-workshop/) carefully).
The heart of the workshop is a web application simulating a fight between [superheroes](https://en.wikipedia.org/wiki/Superhero) and [super-villains](https://en.wikipedia.org/wiki/Supervillain).
There are three exercises:
- Developing and deploying the "**hero**" microservice
- Developing and deploying the "**villain**" microservice
- Developing and deploying the "**fight**" microservice
For more details, I direct you to the [workshop statement](https://cescoffier.github.io/quarkus-openshift-workshop/overview/).
## Requirements
The **Leaderboard** should do two things:
- **encourage participants** by introducing a dose of competition
- **determine the fastest 30 participants** and award them a prize.
In previous editions of this workshop, successful completion was validated on the basis of screenshots sent to a Slack channel.
Participants submitted the screenshots, and the moderator validated them in order, recording the points in a Google Sheet and announcing progress at regular intervals.
A moderator was dedicated to managing the leaderboard.
This year, it was expected that the process would be **fully automated** to avoid these time-consuming administrative tasks.
## How it works
As I said in the introduction, for the creation of this **Leaderboard** I imposed a figure of style on myself: the use of **Prometheus** and **Grafana**.
Prometheus is a **time series** database.
In other words, it is optimised for storing the evolution of numerical data over time and for producing statistics on this data.
Grafana is used to present Prometheus data in the form of dashboards.
These two tools are used extensively in two products that we used for this workshop: **Red Hat OpenShift Container Platform** and **Red Hat Advanced Cluster Management**.
Prometheus is very good at knowing that "*Pod X in namespace Y has just entered the Running* state".
And that's precisely what we're interested in:
- If the **hero-database-1** Pod is created in the **batman-workshop-prod** namespace, then we know that the **batman** user has just finished deploying the **hero** exercise database in the **prod** environment.
- If the Deployment **hero** in the **batman-workshop-prod** namespace changes to the **Available** state, then we know that the **batman** user has successfully deployed his **hero** microservice.
- If a **batman-hero-run-*\<random>*-resync-pod** Pod in the **batman-workshop-dev** namespace changes to the **Completed** state, then we know that the user's last Tekton pipeline has been successfully completed.
And if the three previous conditions are true, we can deduce that the user has completed and validated the **hero** exercise.
Over time, these time series progress as shown in the figure below.
{{< attachedFigure src="exercise-validation.png" title="When the three conditions are met, the exercise is validated." >}}
That's a good start, isn't it?
If you do the same thing for all three exercises, you can see who has completed the whole workshop.
Given that some exercises take longer than others, we could imagine awarding more points to long exercises and fewer to short ones.
This is the approach I've tried to model in the figure below, with a weighting of 55 for the first exercise, 30 for the second and 45 for the last.
The idea is to approximate a linear progression of points over time (1 point per minute).
{{< attachedFigure src="counting-scheme-no-time.png" title="Progression of the number of points for a normal, slow and fast user over time and with each exercise weighted according to the nominal duration of the exercise." >}}
It's starting to come together.
But if you look closely, at the end of the workshop (at the 150th minute), all the participants have finished and have the same score.
And that poses two problems for me:
- Firstly, **Prometheus doesn't know how to sort participants by order of arrival**.
And I don't want to have to analyse the results minute by minute at the prize-giving ceremony to manually note the order of arrival of the participants.
- Then, if all the participants who have completed an exercise have the same score, **where's the thrill of the competition**?
I know that with any SQL database you'd just have to do a `SELECT * FROM users ORDER BY ex3_completion_timestamp ASC` to get the result.
I know I'm trying to use Prometheus for a task that isn't really its job.
But, let's be silly...
Let's dream for a minute...
**How about we try and get around this limitation of Prometheus?**
Couldn't we moderate or accentuate the weighting of an exercise according to the time taken by the user to complete it?
Couldn't an accelerator be activated each time an exercise is validated, giving a few extra points for every minute that passes?
That would make the competition more engaging and more fun!
And that's what I've tried to model in the diagram below.
{{< attachedFigure src="counting-scheme-with-time.png" title="Progression of the number of points for a normal, slow and fast user over time and with accelerator and weighting of each exercise according to the time it takes the user to complete the exercise." >}}
Now the question is: does a user who takes the lead in the first exercise gain a significant advantage that would make the competition unbalanced?
We found the answer during the various rehearsals that took place at Red Hat before D-Day.
{{< attachedFigure src="counting-scheme-dry-run.png" title="Validation of the point counting model during a dry-run." >}}
In the screenshot above, you can see that Batman completed the "hero" exercise **late**.
But by completing the "villain" exercise **very quickly**, he was able to take back the lead... **temporarily**.
Catwoman, who was leading the game, passed him again before Batman regained the lead and held on to it until the last moment.
Phew! What a thriller!
So, **it's definitely possible to start late and catch up**.
The principle is validated!
And now, how do we implement this in Prometheus?
## Implementation in Prometheus
If I had had to develop this point-counting system in a Prometheus pre-configured for production, I would have faced two difficulties:
1. By default, the time resolution of the Prometheus + Grafana suite included in **Red Hat Advanced Cluster Management** is 5 minutes (this corresponds to the minimum time step between two measurements).
Validating the correct counting of points with a resolution of 5 minutes over a 2.5 hour shift takes 2.5 hours (**real speed**).
2. To implement this point-counting system, I need to use **recording rules**.
However, modifying a recording rule **does not automatically trigger the rewriting of time series calculated in the past**.
For these two reasons, I decided to use a specific testing workbench.
### Using a testing workbench
The specific features of this testing workbench are as follows:
- Prometheus scrapping frequency is set to **5 seconds**.
This means that validating the scoring accuracy is done **60 times faster**: 2.5 hours of workshop time is validated in 2 minutes and 30 seconds, with a resolution of 5 minutes.
- At each iteration, Prometheus is reconfigured with the new recording rules, past time series are erased and **Prometheus immediately starts recording new time series from a standardised test data set**.
This makes fine-tuning much easier!
The testing workbench is available in the Git repository [opencodequest-leaderboard](https://github.com/nmasse-itix/opencodequest-leaderboard) and requires only a few pre-requisites: `git`, `bash`, `podman`, `podman-compose` and the `envsubst` command. These dependencies can usually be installed with your distribution's packages (`dnf install git bash podman podman-compose gettext-envsubst` on Fedora).
Get the code for the testing workbench and start it:
```sh
git clone https://github.com/nmasse-itix/opencodequest-leaderboard.git
cd opencodequest-leaderboard
./run.sh
```
The first time you start up, connect to the Grafana interface (`http://localhost:3000`) and carry out these 4 actions:
- Authenticate with login **admin** and password **admin**.
- Set a new administrator password (or just click on **Skip**...)
- Configure a default data source of type **Prometheus** with the following values:
- **Prometheus server URL**: `http://prometheus:9090`
- **Scrape interval**: `5s`.
- Create a new *dashboard* from the **grafana/leaderboard.json** file in the Git repository.
Data should now appear in the Grafana dashboard.
To enjoy this to the full, stop the `run.sh` script by pressing **Ctrl + C** and run it again!
After a few seconds, you should see fresh data appear on the dashboard, as in the video below.
{{< attachedFigure src="leaderboard-simulation.gif" title="Simulation of the Open Code Quest workshop on the testing workbench to validate the point-counting system (video accelerated 10x)." >}}
### Prometheus queries
The Prometheus queries I used are stored in the file `prometheus/recording_rules.yaml.template`.
This is a **template** that contains variables.
The variables are replaced by their values when the `run.sh` script is run.
All requests are recorded in the form of Prometheus **recording rules**.
They are divided into three groups:
1. The `opencodequest_leaderboard_*` queries represent the state of completion of an exercise by a user.
2. The `opencodequest_leaderboard_*_onetime_bonus` requests represent the time bonus acquired by a user who completes an exercise.
3. The `opencodequest_leaderboard_*_lifetime_bonus` queries represent the carry-over of the time bonus acquired by a user who completes an exercise.
#### Queries `opencodequest_leaderboard_*`
The three queries you need to understand first are :
- `opencodequest_leaderboard_hero:prod`: **hero** exercise completion status (0 = not completed, 1 = completed)
- `opencodequest_leaderboard_villain:prod`: **villain** exercise completion status (*ditto*)
- `opencodequest_leaderboard_fight:prod`: **fight** exercise completion status (*ditto*)
These three queries are based on the same model.
I've taken the first one and adapted and formatted it slightly to make it more understandable.
It's almost a valid request.
Before executing it, you'll just have to replace `$EPOCHSECONDS` with the **unix timestamp** of the current time.
```
sum(
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "superman", "","")
) >= bool ($EPOCHSECONDS + 55)
or
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "catwoman", "","")
) >= bool ($EPOCHSECONDS + 50)
or
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "invisibleman", "","")
) >= bool ($EPOCHSECONDS + 60)
or
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "batman", "","")
) >= bool ($EPOCHSECONDS + 65)
) by (user)
```
To replace `$EPOCHSECONDS` with the **unix timestamp** of the current time, you can use a *here-doc* in your favourite Shell:
```sh
cat << EOF
Prometheus query
EOF
```
Copy and paste the query into the **Explore** section of Grafana and you should get the following graph.
{{< attachedFigure src="grafana-explore-opencodequest-leaderboard-hero.png" title="The metric \"opencodequest_leaderboard_hero:prod\" represents the completeness status of the exercise \"hero\" in the environment \"prod\"." >}}
It should be read as follows (note: 1728646377 = 13:32:57):
- **Superman** finishes the hero exercise **50 seconds** after the workshop has started.
- **Catwoman** finishes the hero exercise **55 seconds** after the workshop has started.
- **Invisible Man** finishes the hero exercise **60 seconds** after the workshop has started.
- **Batman** ends the hero exercise **65 seconds** after the workshop has started.
This query works as follows:
- `up{instance="localhost:9090"}` is a time serie which always returns **1**, accompanied by lots of *labels* which are useless for our purposes.
- `label_replace(TIMESERIE, "user", "superman", "", "")` adds the label **user=superman** to the time serie.
- `timestamp(TIMESERIE) >= bool TS` returns **1** for any measurement taken **after** the timestamp TS, 0 otherwise.
- `TIMESERIE1 or TIMESERIE2` merges the two time series.
- `sum(TIMESERIE) by (user)` removes all labels except `user`.
I could have used `min`, `max`, etc. instead of `sum` as I only have one timeserie per **user** value.
The results of these three queries are stored in Prometheus in the form of time series, thanks to the recording rules which define them.
**These represent the test data set which I use to validate that the Leaderboard is working properly**.
In the **Open Code Quest** environment, they will be replaced by real metrics from the OpenShift clusters.
#### Queries `opencodequest_leaderboard_*_onetime_bonus`
The following queries calculate a time bonus for users who complete an exercise.
The earlier the user completes the exercise (in relation to the scheduled end time), the greater the bonus.
Conversely, the later the user is in relation to the scheduled end time, the smaller the bonus.
- `opencodequest_leaderboard_hero_onetime_bonus:prod` represents the time bonus awarded to the user who completes the **hero** exercise.
- `opencodequest_leaderboard_villain_onetime_bonus:prod` represents the time bonus awarded to the user who completes the **villain** exercise.
- `opencodequest_leaderboard_fight_onetime_bonus:prod` represents the time bonus awarded to the user who completes the **fight** exercise.
These three queries are based on the same model.
It may seem complex at first, but in fact it's not that complex.
```
(increase(opencodequest_leaderboard_hero:prod[10s]) >= bool 0.5)
*
(
55
+
sum(
(
${TS_EXERCISE_HERO}
-
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "superman", "","")
or
label_replace(up{instance="localhost:9090"}, "user", "invisibleman", "","")
or
label_replace(up{instance="localhost:9090"}, "user", "catwoman", "","")
or
label_replace(up{instance="localhost:9090"}, "user", "batman", "","")
)
) / 5
) by (user)
)
```
To understand how this query works, I suggest you split it into two parts: the `increase(...)` part on one side and the rest on the other.
We overlay this with the previous query and we get the following figure.
{{< attachedFigure src="grafana-opencodequest-leaderboard-onetime-bonus.png" title="The metric \"opencodequest_leaderboard_hero_onetime_bonus:prod\" represents the time bonus allocated to a user when they complete the \"hero\" exercise in the \"prod\" environment." >}}
From top to bottom, we can see:
1. The `opencodequest_leaderboard_hero:prod` query.
It represents the completeness of the exercise.
2. The `increase(opencodequest_leaderboard_hero:prod[10s]) >= bool 0.5` part detects changes in the state of the previous query.
3. The part `55 + sum(($TS - timestamp(...) / 5) by (user)` represents the evolution of the time bonus over time.
The term **55** is the nominal bonus for the exercise and the divisor **5** is used to vary the bonus **by one unit every 5 seconds**.
4. The total is the application of the time bonus at the moment the user completes the exercise.
#### Queries `opencodequest_leaderboard_*_lifetime_bonus`
The following queries carry forward the time bonus from measurement to measurement until the end of the workshop.
- `opencodequest_leaderboard_hero_lifetime_bonus:prod` represents the carryover of the time bonus awarded to the user who completes the **hero** exercise.
- `opencodequest_leaderboard_villain_lifetime_bonus:prod` represents the carryover of the time bonus awarded to the user who completes the **villain** exercise.
- `opencodequest_leaderboard_fight_lifetime_bonus:prod` represents the carryover of the time bonus awarded to the user who completes the **fight** exercise.
These three queries are based on the same model:
```
sum_over_time(opencodequest_leaderboard_hero_onetime_bonus:prod[1h])
```
The function `sum_over_time(TIMESERIES)` sums the values of the time serie over time.
This can be seen as the integral of the time serie.
The following figure shows how this query works in more detail.
{{< attachedFigure src="grafana-opencodequest-leaderboard-lifetime-bonus.png" title="The metric \"opencodequest_leaderboard_hero_lifetime_bonus:prod\" represents the carry-over of the time bonus allocated to a user when he completes the exercise \"hero\" in the environment \"prod\"." >}}
From top to bottom, we can observe:
1. The `opencodequest_leaderboard_hero:prod` query.
It represents the completeness of the exercise.
2. The query `opencodequest_leaderboard_hero_onetime_bonus:prod`.
This represents the application of the time bonus when the user completes the exercise.
3. The result is the time bonus carried forward from the moment the user completes the exercise.
Note: there is a time difference of one unit between the last query and the first two.
I think this is a consequence of the dependencies between the recording rules.
#### The final query
The final query that determines user score is the sum of 6 components:
- The time bonus for the **hero** exercise (carried over)
- The accelerator activated at the end of the **hero** exercise
- The time bonus for the **villain** exercise (carried over)
- Accelerator activated at the end of the **villain** financial year
- Time bonus for **fight** exercise (postponed)
- Accelerator activated at the end of the **fight** exercise
In the dialect used by Prometheus, this is written as follows:
```
opencodequest_leaderboard_hero_lifetime_bonus:prod
+ sum_over_time(opencodequest_leaderboard_hero:prod[1h])
+ opencodequest_leaderboard_villain_lifetime_bonus:prod
+ sum_over_time(opencodequest_leaderboard_villain:prod[1h])
+ opencodequest_leaderboard_fight_lifetime_bonus:prod
+ sum_over_time(opencodequest_leaderboard_fight:prod[1h])
```
The time bonuses were described in the previous section.
All that remains to explain is how the accelerator works.
The time series `opencodequest_leaderboard_{hero,villain,fight}:prod` is the completeness state of the exercise (binary value: 0 or 1).
To obtain [a ramp](https://en.wikipedia.org/wiki/Ramp_function), you need to take its integral.
So I use the function `sum_over_time(TIMESERIES)` for this.
To make things more complicated, you could imagine changing the slope of the ramp using a multiplier, but I've decided that this isn't necessary.
In fact, the 3 accelerators already add up, so the user gains 1 point every 5 minutes after the **hero** exercise, 2 points after the **villain** exercise and 3 points after the **fight** exercise.
The following figure shows the 6 Prometheus query components used to calculate the user's score.
{{< attachedFigure src="grafana-opencodequest-leaderboard.png" title="The 6 components of the Prometheus query calculating user scores and the final result." >}}
### Recording Rules
The `opencodequest_leaderboard_*` queries use the **increase** function and the `opencodequest_leaderboard_*_lifetime_bonus` queries use the **sum_over_time** function.
These two Prometheus functions have one constraint: they can only be applied **on a range vector** (this is the `timeserie[range]` syntax you saw in the examples above).
And **a range vector cannot be the result of a calculation**.
This means that the following query is valid:
```cpp
// OK
sum_over_time(
opencodequest_leaderboard_hero:prod[1h]
)
```
But these are not:
```cpp
// parse error: ranges only allowed for vector selectors
sum_over_time(
(1 + opencodequest_leaderboard_hero:prod)[1h]
)
// parse error: binary expression must contain only scalar and instant vector types
sum_over_time(
1 + opencodequest_leaderboard_hero:prod[1h]
)
```
This means that it is not possible to build a giant query which calculates the score of all the participants over time.
So each time we use one of these functions that requires a range vector, we have to use a recording rule to materialise the result of the calculation in a named time serie.
And because our queries depend on each other, they have to be placed in different recording rule groups.
This is why you will find three groups of recording rules in the `prometheus/recording_rules.yaml.template` file:
- `opencodequest_base` for the test dataset (which only exists in the testing workbench).
- `opencodequest_step1` for the `opencodequest_leaderboard_*_onetime_bonus` queries.
- `opencodequest_step2` for the `opencodequest_leaderboard_*_lifetime_bonus` queries.
And you'll see in the following article that recording rules in a **Red Hat Advanced Cluster Management** configuration have a few subtleties...
## Creating the Grafana dashboard
Once all the Prometheus queries have been set up, creating the Grafana dashboard is relatively straightforward:
- Create two variables: **env** (the participant environment on which to calculate the score) and **user** (the list of users to be included in the leaderboard).
- Add two visualisations: one for the instant ranking and one for the progression of scores over time.
The **user** variable is multi-valued (you can select all users or uncheck users you don't want to see... like those who were used to test the day before!) and the possible values are taken from the labels of a Prometheus time series (it doesn't matter which one, as long as all users are represented).
The **env** variable has three possible values ("dev", "preprod" or "prod") but you can only select one value at a time.
These two variables are then used in the Leaderboard query in the following way:
```
max(
opencodequest_leaderboard_hero_lifetime_bonus:${env:text}{user=~"${user:regex}"}
+ sum_over_time(opencodequest_leaderboard_hero:${env:text}{user=~"${user:regex}"}[1h])
+ opencodequest_leaderboard_villain_lifetime_bonus:${env:text}{user=~"${user:regex}"}
+ sum_over_time(opencodequest_leaderboard_villain:${env:text}{user=~"${user:regex}"}[1h])
+ opencodequest_leaderboard_fight_lifetime_bonus:${env:text}{user=~"${user:regex}"}
+ sum_over_time(opencodequest_leaderboard_fight:${env:text}{user=~"${user:regex}"}[1h])
) by (user)
```
The `${user:regex}` syntax allows Grafana to replace `user=~"${user:regex}"` with `user=~"(batman|catwoman|invisibleman|superman)"` when several values are selected in the drop-down list.
### Visualising instant ranking
To show the instant ranking, I used the **Bar Chart** visualisation with a **Sort by** transformation on the **Value** field.
{{< attachedFigure src="grafana-opencodequest-leaderboard-instant-snapshot.png" title="Grafana visualisation settings for instant ranking." >}}
The important parameters of this visualisation are :
- **Format**: `Table`
- **Type**: `Instant
- **Legend**: `{{user}}` (to display the participant's name next to their score)
### Viewing scores over time
To track the progression of scores over time, I have opted for the **Time series** visualisation.
{{< attachedFigure src="grafana-opencodequest-leaderboard-points-over-time.png" title="Grafana visualisation settings for score progression." >}}
The important parameters of this visualisation are :
- **Format**: `Time series`
- **Type**: `Range`
- **Min step**: `5s` in the testing workbench and `5m` in real life.
### Result
The dashboard used on the day of Open Code Quest was more or less as shown in Figure 5 (the animated gif):
- The instant ranking, projected from time to time on the overhead projector to announce the scores.
- The progression of scores over time, displayed on a second screen to keep an eye on the competition.
You can find all the Grafana dashboards presented here in the [grafana](https://github.com/nmasse-itix/opencodequest-leaderboard/tree/main/grafana) folder.
## The day of the Open Code Quest
On the day of the Open Code Quest, the Leaderboard worked well and enabled us to determine the fastest 30 participants.
They went up on stage to receive a reward.
As for the question on everyone's lips: did superheroes fight it out for the podium?
The answer is a resounding **YES!**
And there were plenty of thrills when the results were announced...
{{< attachedFigure src="grafana-opencodequest-points.png" title="Progression of the Open Code Quest 74 participants' scores." >}}
Take a look at all those intersecting curves, all those superheroes competing for first place!
## Conclusion
In conclusion, the Open Code Quest was as stimulating an experience for the participants as it was for me as organiser.
The project not only highlighted technologies such as Quarkus, OpenShift and IBM's Granite model, but also demonstrated the extent to which tools such as Prometheus and Grafana can be used creatively to address very real problems.
Designing the Leaderboard, although complex, added a motivating competitive dimension to the workshop.
On the day, watching the participants compete for speed while exploring Red Hat solutions was incredibly gratifying.
To find out how I implemented this Leaderboard in a multi-cluster architecture using Red Hat ACM, please visit: {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md" >}}.

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif

452
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md

@ -0,0 +1,452 @@
---
title: "Behind the scenes at Open Code Quest: how I implemented the Leaderboard in Red Hat Advanced Cluster Management"
date: 2024-11-05T00:00:00+02:00
#lastMod: 2024-10-11T00:00:00+02:00
opensource:
- Kubernetes
- Prometheus
- Grafana
topics:
- Observability
# Featured images for Social Media promotion (sorted from by priority)
#images:
#- counting-scheme-with-time.png
resources:
- '*.png'
- '*.svg'
- '*.gif'
---
After revealing the behind-the-scenes design of the Leaderboard for the "Open Code Quest" workshop during the {{< internalLink path="/speaking/red-hat-summit-connect-france-2024/index.md" >}}, it's time to delve deeper into its practical implementation!
In this article, I'm going to take you through the configuration of **Red Hat Advanced Cluster Management** as well as the various adaptations needed to connect the *Leaderboard* created earlier with the **Open Code Quest** infrastructure.
Come on board with me for this new stage, which is more technical than the previous one, as I had to get creative to wire up a very "conceptual" Grafana dashboard with the reality of OpenShift clusters!
<!--more-->
This article follows on from {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md" >}}.
If you haven't read it yet, I advise you to read it first to understand the context better.
## Prometheus queries
In the previous article, I discussed how we could detect the actions of a user in his environment:
- If the **hero-database-1** Pod is created in the **batman-workshop-prod** namespace, then we know that the **batman** user has just finished deploying the **hero** exercise database in the **prod** environment.
- If the Deployment **hero** in the **batman-workshop-prod** namespace changes to the **Available** state, then we know that the **batman** user has successfully deployed his **hero** microservice.
- If a **batman-hero-run-*\<random>*-resync-pod** Pod in the **batman-workshop-dev** namespace changes to the **Completed** state, then we know that the user's last Tekton pipeline has been successfully completed.
If the three previous conditions are true, we can deduce that the user has completed and validated the **hero** exercise.
The reality is in fact a little more complicated, because between the very conceptual *Leaderboard* of the previous article and these very technical elements, it was necessary to make quite a few adaptations.
In the end, for each exercise I had to implement three Prometheus queries to detect the three conditions above.
Fortunately, all three exercises are based on the same model, so the set of queries is very similar for all three exercises.
### Detecting the Quarkus micro-service
I detect the deployment of the Quarkus microservice **hero** in the environment **dev** using the following query, which I persist as a recording rule named **opencodequest_hero_quarkus_pod:dev**.
```
clamp_max(
sum(
label_replace(kube_deployment_status_condition{namespace=~"[a-zA-Z0-9]+-workshop-dev",deployment="hero",condition="Available",status="true"}, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-dev")
) by (user),
1)
or
clamp(
sum(
label_replace(kube_namespace_status_phase{namespace=~"[a-zA-Z0-9]+-workshop-(dev|preprod|prod)",phase="Active"}, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-(dev|preprod|prod)")
) by (user),
0, 0)
```
This query is in two parts.
The first part works as follows:
- `kube_deployment_status_condition{namespace=~"[a-zA-Z0-9]+-workshop-dev",deployment="hero",condition="Available",status="true"}` returns the number of kubernetes **Deployment** with the name **hero**, in a namespace ending in **-workshop-dev** and being in a **Available** state.
- `label_replace(TIMESERIE, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-dev")` extracts the user's name from the **namespace** label using a regular expression and stores it in a **user** label.
- `sum(TIMESERIE) by (user)` deletes all labels except **user** (I could have used `min`, `max`, etc, that works too).
- `clamp_max(TIMESERIE, 1)` caps the result at 1 to ensure that the result is binary.
This first part returns the state of the Quarkus microservice **as soon as the kubernetes Deployment exists**.
As long as kubernetes Deployment does not exist, no data is returned by this part of the query.
The second part of the query addresses this problem:
- `kube_namespace_status_phase{namespace=~"[a-zA-Z0-9]+-workshop-(dev|preprod|prod)",phase="Active"}` returns the namespaces of participants who are in an active state (they all are during the workshop).
- `label_replace(TIMESERIE, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-(dev|preprod|prod)")` extracts the name of the user from the **namespace** label using a regular expression and stores it in a **user** label.
- `sum(TIMESERIE) by (user)` deletes all labels except **user** (I could have used `min`, `max`, etc, that works too).
- `clamp(TIMESERIES, 0, 0)` forces all values in the time serie to 0.
This second part makes it possible to have a default value (0) for all participants, even when Kubernetes Deployment is not yet present.
The `or` keyword in the middle of the two queries merges the two parts, with the first taking precedence over the second.
The **villain** and **fight** microservices, as well as the **preprod** and **prod** environments, are based on the same principle.
In total, 9 time series are recorded in the form of recording rules:
- `opencodequest_hero_quarkus_pod:dev`
- `opencodequest_hero_quarkus_pod:preprod`
- `opencodequest_hero_quarkus_pod:prod`
- `opencodequest_villain_quarkus_pod:dev`
- `opencodequest_villain_quarkus_pod:preprod`
- `opencodequest_villain_quarkus_pod:prod`
- `opencodequest_fight_quarkus_pod:dev`
- `opencodequest_fight_quarkus_pod:preprod`
- `opencodequest_fight_quarkus_pod:prod`
### Detecting the database
I detect the deployment of the **hero** database in the **dev** environment using the following query, which I persist as a recording rule named **opencodequest_hero_db_pod:dev**.
```
clamp_max(
sum(
label_replace(kube_pod_status_phase{namespace=~"[a-zA-Z0-9]+-workshop-dev",pod="hero-database-1",phase="Running"}, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-dev")
) by (user),
1)
or
clamp(
sum(
label_replace(kube_namespace_status_phase{namespace=~".*-workshop-(dev|preprod|prod)",phase="Active"}, "user", "$1", "namespace", "(.*)-workshop-(dev|preprod|prod)")
) by (user),
0, 0)
```
The query is very similar to the previous one, except that I'm basing it on the state of the **Pod** named **hero-database-1**.
This is why I'm using the **kube_pod_status_phase** timeseries.
The **villain** microservice and the **preprod** and **prod** environments are based on the same principle.
In total, 6 time series are recorded in the form of recording rules (**fight** has no database):
- `opencodequest_hero_db_pod:dev`
- `opencodequest_hero_db_pod:preprod`
- `opencodequest_hero_db_pod:prod`
- `opencodequest_villain_db_pod:dev`
- `opencodequest_villain_db_pod:preprod`
- `opencodequest_villain_db_pod:prod`
The recording rules for the **prod** environment are a little different because in this environment the database is shared between all the participants and deployed before the workshop starts with the rest of the infrastructure.
Consequently, I force the value of the time series `opencodequest_hero_db_pod:prod` and `opencodequest_villain_db_pod:prod` to 1 using a variant of the second part of the query explained above:
```
clamp(
sum(
label_replace(kube_namespace_status_phase{namespace=~".*-workshop-(dev|preprod|prod)",phase="Active"}, "user", "$1", "namespace", "(.*)-workshop-(dev|preprod|prod)")
) by (user),
1, 1)
```
### Detecting the end of the Tekton Pipeline
Detecting the end of the Tekton pipeline required more work because there is no standard metric for knowing the state of a pipeline.
I therefore relied on the presence of a `<user>-hero-run-<random>-resync-pod` pod in the user's **dev** environment.
This pod corresponds to the last stage of the Tekton Pipeline.
So if this pod is in a **Completed** state, it means that the Pipeline has completed successfully.
I detect the state of the Tekton Pipeline **hero** in the **dev** environment using the following query, which I persist in the form of a recording rule called **opencodequest_hero_pipeline**.
```
clamp_max(
sum(
label_replace(kube_pod_status_phase{namespace=~"[a-zA-Z0-9]+-workshop-dev",pod=~"[a-zA-Z0-9]+-hero-run-.*-resync-pod",phase="Succeeded"}, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-dev")
) by (user),
1)
or
clamp(
sum(
label_replace(kube_namespace_status_phase{namespace=~".*-workshop-(dev|preprod|prod)",phase="Active"}, "user", "$1", "namespace", "(.*)-workshop-(dev|preprod|prod)")
) by (user),
0, 0)
```
The query is very similar to the previous one, except that the expected state of the Pod is different (**Completed**) and the name of the Pod is different.
The **villain** and **fight** microservices are based on the same principle.
In total, 3 time series are recorded in the form of recording rules (pipelines only exist in the **dev** environment):
- `opencodequest_hero_pipeline`
- `opencodequest_villain_pipeline`
- `opencodequest_fight_pipeline`
### Detecting the end of the exercise
To detect the end of the **hero** exercise in the **dev** environment, I combine the results of the three previous queries using the following query, which I persist in the form of a recording rule called **opencodequest_leaderboard_hero:dev**.
```
max(
(opencodequest_hero_quarkus_pod:dev + opencodequest_hero_db_pod:dev + opencodequest_hero_pipeline) == bool 3
) by (user, cluster)
```
This query works as follows:
- `(opencodequest_hero_quarkus_pod:dev + opencodequest_hero_db_pod:dev + opencodequest_hero_pipeline) == bool 3` returns 1 when all three components of the exercise are validated, 0 otherwise.
The **bool** operator is important because without it, the query would not return any results until all three components of the exercise have been validated.
- `max(TIMESERIE) by (user, cluster)` eliminates all labels except **cluster** and **user**.
Here, the use of the `max` function is useful for preserving the maximum level of completeness of the exercise if, for example, the user started the exercise on one cluster and then finished it on another cluster.
This shouldn't happen, but if in doubt...
The **fight** exercise only has two components because it doesn't have a database.
The queries concerning it will therefore be simpler:
```
max(
(opencodequest_fight_quarkus_pod:prod + opencodequest_fight_pipeline) == bool 2
) by (user, cluster)
```
There are a total of 9 *recording rules* which record the state of completion of the 3 exercises across the 3 environments of the participants.
- `opencodequest_leaderboard_hero:dev`
- `opencodequest_leaderboard_hero:preprod`
- `opencodequest_leaderboard_hero:prod`
- `opencodequest_leaderboard_villain:dev`
- `opencodequest_leaderboard_villain:preprod`
- `opencodequest_leaderboard_villain:prod`
- `opencodequest_leaderboard_fight:dev`
- `opencodequest_leaderboard_fight:preprod`
- `opencodequest_leaderboard_fight:prod`
And with these last recording rules we've just connected the Leaderboard with the OpenShift environments used for the **Open Code Quest**.
Now let's see how observability has been implemented in **Red Hat Advanced Cluster Management**!
## Observability in Red Hat Advanced Cluster Management
During the Open Code Quest, we had 8 clusters at our disposal:
- 1 **central** cluster
- 1 cluster for artificial intelligence
- 6 clusters distributed among the participants (we had planned one cluster per table)
**Red Hat Advanced Cluster Management** is installed on the **central** cluster and from there it controls all the clusters.
Observability is an additional module (in the sense that it is not installed by default) of **Red Hat Advanced Cluster Management** and this module is based on the Open Source components **Prometheus**, **Thanos** and **Grafana**.
The following diagram shows the architecture of the observability module in **Red Hat Advanced Cluster Management**.
I created it by observing the relationships between the components from an installation of ACM version 2.11.
{{< attachedFigure src="redhat-acm-observability-architecture.svg" title="Logical architecture of observability in Red Hat Advanced Cluster Management 2.11" >}}
The components deployed on the central cluster are in **green**, those deployed on the managed clusters are in **blue** and the configuration items are in **grey**.
I've also illustrated the two possible places for calculating *recording rules*, in **yellow**.
Note that ConfigMaps on managed clusters can be deployed automatically from the **central** cluster via a **ManifestWork**.
### Implementation of the recording rules
Recording rules can be calculated at two different times:
- In each managed cluster, before sending to the central cluster.
- In the central cluster, after reception.
But there's a little subtlety: this choice is true for standard OpenShift metrics.
The recording rules using *custom* metrics (i.e. **User Workload Monitoring**) are calculated **only after reception on the central cluster**.
It is not possible to calculate them before sending them to the central cluster.
You can only specify *custom* metrics to be sent as-is.
They are not configured in the same place either, depending on whether it's a *custom* metric or a standard metric and whether it's done before or after sending.
To help you, I've put together a summary table:
| Type of metric | Computation of the recording rule | Location of the configuration | Name of the ConfigMap | Key |
| -------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------- | ----------------------- |
| standard | before sending | `open-cluster-management-observability` namespace on the **central** cluster or managed clusters | `observability-metrics-custom-allowlist` | `metrics_list.yaml` |
| custom | **no computation**, sent as-is | `open-cluster-management-observability` namespace on the **central** cluster or managed clusters | `observability-metrics-custom-allowlist` | `uwl_metrics_list.yaml` |
| standard or custom | on arrival | `open-cluster-management-observability` namespace on the **central** cluster | `thanos-ruler-custom-rules` | `custom_rules.yaml` |
#### Computing Recording Rules before sending
To send metrics and compute recording rules **before sending** to the **central** cluster, this is configured in the `open-cluster-management-observability` namespace on the **central** cluster via a ConfigMap :
```yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: observability-metrics-custom-allowlist
namespace: open-cluster-management-observability
data:
uwl_metrics_list.yaml: |
names:
- fights_total
metrics_list.yaml: |
names:
- kube_deployment_status_replicas_ready
- kube_pod_status_phase
- kube_namespace_status_phase
rules:
- record: opencodequest_hero_quarkus_pod:dev
expr: kube_deployment_status_condition{namespace=~\"[a-zA-Z0-9]+-workshop-dev\",deployment=\"hero\",condition=\"Available\",status=\"true\"}
```
The configuration above allows you to :
- Send the `fights_total` custom metric as is.
- Send the standard `kube_deployment_status_replicas_ready`, `kube_pod_status_phase` and `kube_namespace_status_phase` metrics as is.
- Create an `opencodequest_hero_quarkus_pod:dev` metric from the Prometheus query `kube_deployment_status_condition{...}` and send the result.
When this ConfigMap is created on the **central** cluster, it is automatically replicated to all managed clusters.
According to the documentation, it is also possible to create it in each managed cluster to customise the configuration per cluster.
#### Computing Recording Rules on arrival
For the computation of recording rules **on arrival** on the **central** cluster, this is also configured in the `open-cluster-management-observability` namespace on the **central** cluster, but via another ConfigMap:
```yaml
kind: ConfigMap
apiVersion: v1
metadata:
name: thanos-ruler-custom-rules
namespace: open-cluster-management-observability
data:
custom_rules.yaml: |
groups:
- name: opencodequest
rules:
- record: opencodequest_hero_quarkus_pod:dev
expr: kube_deployment_status_condition{namespace=~"[a-zA-Z0-9]+-workshop-dev",deployment="hero",condition="Available",status="true"}
```
Note that the syntax of the two ConfigMaps is not identical.
- In the `observability-metrics-custom-allowlist` ConfigMap, *double quotes* must be escaped, using a *backslash*.
This is not the case in the other ConfigMap.
- The syntax of the `thanos-ruler-custom-rules` ConfigMap allows groups of *recording rules* to be specified, whereas the other ConfigMap does not.
Note: the names of the metrics in the examples above are more or less fictitious.
These are not the configurations I used in the end.
#### Implementation choices
I have chosen to compute, in the form of recording rules **in managed clusters**, the three components that make it possible to validate the completeness of an exercise, i.e.:
- The **Deployment** of the Quarkus microservice is in the **Available** state.
- The **Pod** of the database, when there is one, is present and in a **Ready** state.
- The Tekton Pipeline of the microservice has been successfully completed.
As there is no standard metric for Tekton Pipelines, the *recording rule* detects the presence of the **Pod** corresponding to the last stage of the Pipeline and checks that it is in a **Completed** state.
I've created these recording rules for the **dev**, **preprod** and **prod** environments of the participants.
This way, if on the day of the Open Code Quest we had a widespread problem in the **prod** environment, we could quickly switch the computation of the scores to another upstream environment.
I can see one advantage to this approach: computing the three components of each exercise in the managed clusters means that not too many metrics are sent back to the **central** cluster.
In contrast, I had to compute the Leaderboard Prometheus queries described in the first part of this article in the form of recording rules at the **central cluster** level.
I didn't really have much choice: I needed several groups of recording rules and this function is only available in the ConfigMap which configures the recording rules for the **central** cluster.
You can find all the recording rules used for Open Code Quest in the [acm](https://github.com/nmasse-itix/opencodequest-leaderboard/tree/main/acm) folder.
### Setting up observability
Deploying the observability module on the **central** cluster is very simple, and can be done by following [the documentation](https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.11/html/observability/observing-environments-intro#enabling-observability-service):
- Create the namespace `open-cluster-management-observability`.
- Create the pull secret allowing images to be uploaded to **registry.redhat.io**.
- Create an S3 bucket.
- Create the *Custom Resource Definition* `MultiClusterObservability`.
To perform these operations, I used the following commands:
```sh
AWS_ACCESS_KEY_ID="REDACTED"
AWS_SECRET_ACCESS_KEY="REDACTED"
S3_BUCKET_NAME="REDACTED"
AWS_REGION="eu-west-3"
# Create the open-cluster-management-observability namespace
oc create namespace open-cluster-management-observability
# Copy the pull secret from the openshift namespace
DOCKER_CONFIG_JSON=`oc extract secret/pull-secret -n openshift-config --to=-`
echo $DOCKER_CONFIG_JSON
oc create secret generic multiclusterhub-operator-pull-secret \
   -n open-cluster-management-observability \
   --from-literal=.dockerconfigjson="$DOCKER_CONFIG_JSON" \
   --type=kubernetes.io/dockerconfigjson
# Create an S3 bucket
aws s3api create-bucket --bucket "$S3_BUCKET_NAME" --create-bucket-configuration "LocationConstraint=$AWS_REGION" --region "$AWS_REGION" --output json
# Deploy the observability add-on
oc apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
name: thanos-object-storage
namespace: open-cluster-management-observability
type: Opaque
stringData:
thanos.yaml: |
type: s3
config:
bucket: $S3_BUCKET_NAME
endpoint: s3.$AWS_REGION.amazonaws.com
insecure: false
access_key: $AWS_ACCESS_KEY_ID
secret_key: $AWS_SECRET_ACCESS_KEY
EOF
oc apply -f - <<EOF
apiVersion: observability.open-cluster-management.io/v1beta2
kind: MultiClusterObservability
metadata:
name: observability
namespace: open-cluster-management-observability
spec:
observabilityAddonSpec: {}
storageConfig:
metricObjectStorage:
name: thanos-object-storage
key: thanos.yaml
EOF
```
After installing the observability module, the managed clusters are automatically configured to report the most important Prometheus metrics to the **central** cluster.
The **Open Code Quest** workshop takes advantage of custom metrics that I use in the Leaderboard to find out which participants have got their microservices working.
To collect these metrics, I activate the **User Workload Monitoring** feature in each managed **OpenShift** cluster.
```sh
oc -n openshift-monitoring get configmap cluster-monitoring-config -o yaml | sed -r 's/(\senableUserWorkload:\s).*/\1true/' | oc apply -f -
```
### Deploying a Grafana development instance
A Grafana instance is automatically deployed with the observability module, but this instance is read-only: the standard dashboards can be consulted, but new ones cannot be created.
To create new dashboards, you need to deploy a development instance of Grafana at the same time.
```sh
git clone https://github.com/stolostron/multicluster-observability-operator.git
cd multicluster-observability-operator/tools
./setup-grafana-dev.sh --deploy
```
Once the instance has been deployed, you need to connect to it with any OpenShift user and give that user **administrator** privileges.
```sh
GRAFANA_DEV_HOSTNAME="$(oc get route grafana-dev -n open-cluster-management-observability -o jsonpath='{.status.ingress[0].host}')"
echo "Now login to Grafana with your openshift user at https://$GRAFANA_DEV_HOSTNAME"
read -q "?press any key to continue "
./switch-to-grafana-admin.sh "$(oc whoami)"
```
Then create the "Red Hat Summit Connect 2024" dashboard, as explained in the article {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md" >}}.
And finally, export the dashboard in the form of a ConfigMap.
```sh
./generate-dashboard-configmap-yaml.sh "Red Hat Summit Connect 2024"
```
The `red-hat-summit-connect-2024.yaml` file is created.
Simply apply it to the **central** cluster and the dashboard will appear in the production Grafana instance.
```sh
oc apply -f red-hat-summit-connect-2024.yaml
```
## Conclusion
To conclude, implementing the Leaderboard in Red Hat Advanced Cluster Management gave me a better understanding of how observability works, in particular recording rules.
In the end, I have been able to set up a dashboard that tracks the progress of participants in real time.
You can find all the recording rules used for Open Code Quest in the [acm](https://github.com/nmasse-itix/opencodequest-leaderboard/tree/main/acm) folder in the Git repository.

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png

1
content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg

@ -0,0 +1 @@
../../../french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg

BIN
content/english/blog/build-multi-architecture-container-images-with-kubernetes-buildah-tekton-and-qemu/quay-repository.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 130 B

BIN
content/english/blog/build-multi-architecture-container-images-with-kubernetes-buildah-tekton-and-qemu/quay-robot-account.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 130 B

7
content/english/blog/build-multi-architecture-container-images-with-kubernetes-buildah-tekton-aws/index.md

@ -1,6 +1,7 @@
--- ---
title: "Build multi-architecture container images with OpenShift, Buildah and Tekton on AWS" title: "Build multi-architecture container images with OpenShift, Buildah and Tekton on AWS"
date: 2024-05-02T00:00:00+02:00 date: 2024-05-02T00:00:00+02:00
lastMod: 2024-08-30T00:00:00+02:00
opensource: opensource:
- Kubernetes - Kubernetes
- Tekton - Tekton
@ -420,6 +421,12 @@ The [OpenShift 4.15 documentation](https://docs.openshift.com/container-platform
Install the **AWS EFS CSI Driver Operator**. Install the **AWS EFS CSI Driver Operator**.
```yaml ```yaml
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: aws-efs-csi-driver-operator
namespace: openshift-cluster-csi-drivers
---
apiVersion: operators.coreos.com/v1alpha1 apiVersion: operators.coreos.com/v1alpha1
kind: Subscription kind: Subscription
metadata: metadata:

BIN
content/english/blog/configure-redhat-sso-3scale-cli/openid-connect-issuer.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/consistent-dns-name-resolution-for-virtual-machines-and-containers/consistent-dns-resolution-in-vm-and-containers.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/deploy-openshift-single-node-in-kvm/assisted-installer-1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/deploy-openshift-single-node-in-kvm/assisted-installer-2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 130 B

BIN
content/english/blog/deploy-openshift-single-node-in-kvm/assisted-installer-5.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 130 B

BIN
content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/k6-grafana-dashboard.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/k6-simple-test.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/k6-simple-test2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/how-to-run-performance-tests-with-k6-prometheus-grafana/prometheus-targets.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 130 B

BIN
content/english/blog/jmeter-assess-software-performances/control1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/jmeter-assess-software-performances/control2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/jmeter-assess-software-performances/control3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/jmeter-assess-software-performances/if.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/jmeter-assess-software-performances/thread-group.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/jmeter-assess-software-performances/udv.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/nginx-with-tls-on-openwrt/make-menuconfig.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/auth-ok.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 497 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/auth.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 527 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/authorized-domains.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 130 B

BIN
content/english/blog/use-google-account-openid-connect-provider/create-credentials.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/create-project.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/links.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/oauth-consent.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/project-name.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/redirect-uri.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/script-start.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/use-google-account-openid-connect-provider/script-url.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/github-add-webhook.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/github-webhook.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/openshift-buildconfig-webhook.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/writing-workshop-instructions-with-hugo-deploy-openshift/openshift-rebuild.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/blog/writing-workshop-instructions-with-hugo/hugo-screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/apidays-2017/tweet-RedHatFrance.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/apidays-2017/tweet-jylls35.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/devoxx-france-2019/tweet-PetitMel_issa.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/devoxx-france-2019/tweet-cgodard.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/devoxx-france-2019/tweet-gbloquel.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/devoxx-france-2019/tweet-lbroudoux.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 426 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/devoxx-france-2019/tweet-sebi2706.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 KiB

After

Width:  |  Height:  |  Size: 131 B

BIN
content/english/speaking/red-hat-forum-2017/tweet-ylebihan.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 131 B

156
content/english/speaking/red-hat-summit-connect-france-2024/index.md

@ -0,0 +1,156 @@
---
title: "Red Hat Summit Connect France 2024"
date: 2024-10-08T00:00:00+02:00
draft: false
resources:
- '*.jpeg'
- '*.png'
- '*.mp4'
# Featured images for Social Media promotion (sorted from by priority)
images:
- open-code-quest-microservices.png
- rhel-booth-mission-impossible-demo.jpeg
- rhel-booth-mission-impossible-demo-3.jpeg
topics:
- Developer Relations
- Site Reliability Engineer
- Artificial Intelligence
- Edge Computing
---
On October 8, 2024, I took part in the [Red Hat Summit Connect France 2024](https://www.redhat.com/fr/summit/connect/emea/paris-2024) event in a double role:
- I was in charge of the Leaderboard for the **Open Code Quest** workshop and I acted as SRE for the platform of this workshop.
- I was present on the RHEL stand to present our "Mission Impossible" demo with the Lego train.
<!--more-->
## Open Code Quest: a technological and heroic adventure
The **Open Code Quest** workshop brought together technology enthusiasts in an immersive setting that blended technological innovation with the world of superheroes.
The aim was to offer participants an in-depth discovery of Quarkus, OpenShift and OpenShift AI, sprinkled with a pinch of security and a seamless developer experience.
All while immersing them in a captivating adventure where every exercise involved superheroes.
During the workshop, participants had to develop no less than four microservices to build an application simulating combat between superheroes and super villains.
{{< attachedFigure src="open-code-quest-microservices.png" >}}
The microservices were developed in Quarkus, the native Java framework for the cloud, demonstrating how it can transform application development by combining speed of development, lightness and performance.
In particular, Quarkus significantly reduces the memory footprint of applications, while enabling them to start up almost instantaneously.
We have also positioned [Red Hat Developer Hub](https://developers.redhat.com/rhdh/overview), the Red Hat distribution of **Backstage**, an open source platform developed by Spotify to improve the management of complex environments, as the flagship product.
**Red Hat Developer Hub** captured the attention of attendees by offering a unified interface for central management of microservices, CI/CD pipelines and other essential development tools.
Its extensibility made it easy to integrate plugins tailored to the needs of the workshop, simplifying the application lifecycle.
For both developers and architects, **Red Hat Developer Hub** has proved to be a valuable tool, facilitating collaboration and providing a clear view of the infrastructure while improving productivity.
At **Open Code Quest**, we also highlighted [Red Hat Trusted Application Pipelines](https://www.redhat.com/en/products/trusted-application-pipeline), a product designed to secure and automate the software supply chain.
Based on the **Tekton Chains** and **Sigstore** technologies, this product offers complete traceability and guarantees the integrity of software components at every stage of the CI/CD pipeline.
Attendees were able to discover how these tools enhance the security of deployments by providing proof of compliance and transparency on the dependencies used in applications.
I'll let you discover the full list of tools used in the **Open Code Quest** workshop:
{{< attachedFigure src="open-code-quest-namespaces.png" >}}
Before and during the **Open Code Quest**, the administration of the **platform** played a key role in the success of the event.
As an organising member, I was responsible, along with [Sébastien Lallemand](https://sebastienlallemand.net/), for preparing, sizing, installing and configuring the eight OpenShift clusters needed for the workshops to run smoothly.
This included a central cluster, one dedicated to AI, and six others reserved for the participants for their missions.
This crucial preparation phase ensured a stable, high-performance infrastructure.
During the event, my role as SRE (Site Reliability Engineer) was to closely monitor critical metrics, such as resource utilisation, to ensure a smooth and optimal experience for all participants.
Thanks to this proactive monitoring, we were able to offer constant availability of the environments and thus facilitate the smooth running of the workshop.
{{< attachedFigure src="open-code-quest-clusters.png" >}}
Another challenge I tackled for the Open Code Quest was the creation of a **Leaderboard** designed to encourage emulation between participants.
This project required me to think outside the box, as I had to use tools such as **Prometheus** and **Grafana** for a task they weren't designed for: sorting participants by order of finish.
By circumventing the limitations of these monitoring technologies, I used my creativity to design a live ranking system.
Despite the technical complexity, the result exceeded our expectations: the Leaderboard stimulated (friendly) competition between participants, adding a dynamic and engaging dimension to the event.
For us,**Open Code Quest** was much more than just a workshop. It was a day where experts and beginners could exchange ideas, learn and have fun together, while discovering useful technologies for developers and architects.
Whether it was accelerating development with Quarkus, improving the developer experience with Red Hat Developer Hub, managing the security of the supply chain with **Red Hat Trusted Application Pipelines** or using AI with Quarkus, each tool provided concrete value, demonstrated through the exercises.
We also had the opportunity to create an environment that encouraged networking, where participants were able to exchange ideas with experts and their peers.
As a member of the organising team, I'm extremely proud of the success of the **Open Code Quest**.
This workshop showed that it is possible to combine technical learning and entertainment in an immersive and stimulating environment. We would like to thank all the participants for their commitment and enthusiasm, as well as our partners for their support. We look forward to seeing you at future events as we continue to explore together the technological innovations that are transforming our world.
Want to find out more about the Leaderboard?
How did I take into account the specific features of Prometheus when designing the Leaderboard?
How have I calibrated the bonuses and accelerators to encourage competition and emulation?
Everything is explained in these two articles:
1. {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md" >}}
2. {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md" >}}
## "Mission Impossible" demo: Lego, AI & Edge Computing
For part of the day, I was on the RHEL booth, accompanied by [Adrien](https://www.linkedin.com/in/adrien-legros-78674a133/), [Mourad](https://www.linkedin.com/in/mourad-ouachani-0734218/) and [Pauline](https://www.linkedin.com/in/trg-pauline/) to install the "Mission Impossible" demo and answer questions from the public.
We designed this demo for the {{< internalLink path="/speaking/platform-day-2024/index.md" >}} event based on the latest opus of the movie **Mission Impossible: Dead Reckoning**.
In this demo, **Ethan Hunt** needs help to stop the **Lego City #60337** train before it's too late!
Nothing less than the fate of humanity is at stake!
{{< attachedFigure src="mission-impossible-plot.png" >}}
The scenario requires **Ethan Hunt** to board the train to connect a **Nvidia Jetson Orin Nano** card to the train's computer network and deploy an AI that will recognise the traffic signs and stop the train on time before it derails!
A console will provide a remote view of the train's video surveillance camera, with the results of the AI model's inference overlaid.
{{< attachedFigure src="mission-impossible-scenario.png" >}}
To run this demo, we equipped the **Lego** train with a **Nvidia Jetson Orin Nano** card, a webcam and a portable battery.
The Nvidia Jetson Orin card is a System On Chip (SoC), it includes all the hardware that **Ethan Hunt** needs for its mission: CPU, RAM, storage...
Plus a GPU to speed up the calculations!
The Jetson receives the video stream from the onboard camera and transmits orders to the **Lego** Hub via the **Bluetooth Low Energy** protocol.
It is powered by a portable battery for the duration of the mission.
{{< attachedFigure src="rhel-booth-mission-impossible-demo.jpeg" >}}
We are in an Edge Computing context.
On the Jetson, we have installed **Red Hat Device Edge**.
This is a variant of Red Hat Enterprise Linux adapted to the constraints of **Edge Computing**.
We installed **Microshift**, Red Hat's Kubernetes tailored for the Edge.
And using Microshift, we deployed *over-the-air* microservices, an **MQTT broker** and the artificial intelligence model.
The Jetson is connected, for the duration of the mission, to an OpenShift cluster in the AWS cloud via a 5G connection.
In the AWS cloud, there is a RHEL 9 VM that we can use to build the **Red Hat Device Edge** images for the Jetson SoC.
In the OpenShift cluster, the video surveillance application that broadcasts the video stream from the train's on-board camera.
The video stream is broadcast from the Jetson via a **Kafka broker**!
On top of this, there are MLops pipelines to train the AI model.
And finally CI/CD pipelines to build the container images of our microservices for x86 and ARM architectures.
{{< attachedFigure src="mission-impossible-hardware-architecture.png" >}}
To enable **Ethan Hunt** to carry out its mission successfully, we had to guarantee end-to-end data transmission.
To do this, we implemented five services that communicate via an asynchronous message transmission system (**MQTT**).
The first service captures ten images per second at regular intervals.
Each image is resized to 600x400 pixels and encapsulated in an event with a unique identifier.
This event is transmitted to the AI model, which enriches it with the result of the prediction.
The latter is transmitted to a transformation service whose role is to extract the train's action, transmit it to the train controller to slow down or stop the train and at the same time send the event to the streaming service (**Kafka**) deployed on a remote Openshift, which displays the images and the prediction in real time.
{{< attachedFigure src="mission-impossible-software-architecture.png" >}}
And finally, we had to build an artificial intelligence model.
To do this, we followed best practices for managing the model's lifecycle, known as **MLOps**:
- **Acquire the data**: We used an open source dataset containing data from an on-board camera mounted on a car, which was annotated with the signs encountered on its route.
The photos were taken on roads in the European Union and therefore show "standard" road signs (potentially slightly different from **Lego** signs).
- **Develop an AI model**: We chose a learning algorithm and trained the model on an OpenShift cluster with GPUs to speed up the calculation.
- **Deploying the model**: We deployed the model in an inference server for consumption via APIs.
The model had to be integrated into the software architecture (via MQTT).
- **Measure performance and re-train**: By observing the model's behaviour, we were able to measure the quality of the predictions and note that not all **Lego** panels were well recognised.
We decided to re-train the model by refining it with an enriched dataset.
{{< attachedFigure src="mission-impossible-ai.png" >}}
If you weren't able to come and see us on the stand, you can catch up with us in the video below (captured during the {{< internalLink path="/speaking/platform-day-2024/index.md" >}}).
You can see the train stop when it detects the corresponding road sign.
{{< embeddedVideo src="mission-impossible-demo.mp4" autoplay="true" loop="true" muted="true" width="1920" height="1080" >}}
This demonstration shows the relevance of Red Hat solutions for carrying out large-scale IT projects combining **Artificial Intelligence** and **Edge Computing**.
## Conclusion
Through the **Open Code Quest** workshop and the captivating demonstration of the **Lego** train, participants were able to explore innovative solutions for application development, Artificial Intelligence, Edge Computing and Supply Chain security.
All the work done on the platform and the originality of the Leaderboard helped to energise the event, reinforcing the friendly competition between participants while offering them a technical and human experience that we hope will be unforgettable.
For me, this Red Hat Summit Connect was an opportunity to highlight the importance of technologies like Quarkus and OpenShift, but also to share a collective adventure where each participant was able to leave with new skills, inspiration and the desire to continue exploring these solutions.
We hope to continue developing this event to offer even more challenges and innovations to the developer, architect and engineering communities.
See you soon for more technological adventures!

3
content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-ai.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:117fe40fe5f7ebf6f89e1af4b5a50f2cd86d0065ce093136c91258e980c5fac6
size 282668

1
content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-demo.mp4

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/mission-impossible-demo.mp4

3
content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-hardware-architecture.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8e58154ddc59ce0a0f5cd574bde646ff81141fdee6f1eea37aa1b80dd27b1ed
size 291854

3
content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-plot.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9bf7ad564f6d33064c9198ae7d8b2a583f51a34b5ee6c31b8b08b73b67310bea
size 175922

3
content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-scenario.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:55542382ebae2c078bb26fefaa51e1945fd16f01c4c10c64e3e3fa5050dc338d
size 635628

1
content/english/speaking/red-hat-summit-connect-france-2024/mission-impossible-software-architecture.png

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/mission-impossible-software-architecture.png

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-clusters.png

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-clusters.png

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-microservices.png

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-microservices.png

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-namespaces.png

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-namespaces.png

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage-2.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage-2.jpeg

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage.jpeg

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-testimonial.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-testimonial.jpeg

1
content/english/speaking/red-hat-summit-connect-france-2024/open-code-quest-winners.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/open-code-quest-winners.jpeg

1
content/english/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-2.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-2.jpeg

1
content/english/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-3.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-3.jpeg

1
content/english/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo.jpeg

@ -0,0 +1 @@
../../../french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo.jpeg

BIN
content/english/speaking/red-hat-tech-exchange-2019/2019-10-21-RHTE-Award.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 130 B

BIN
content/english/speaking/techweek-sg-2019/tweet-YadaYac.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 131 B

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2ff6860a11084f46c66a6a97b894bd2d862926cf0c9556713d40930f4201e4f1
size 46993

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7140091b707a1c40b560242d3177f3e078540836ab699228f99ac421f6445287
size 160790

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3bae94879ffedec0bf6298e8d2ca24b79401e78b9ef2055cb8d2f2d0ed3cdf0d
size 155225

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f26b11e956df0f72af3aa6391edb86d603872ca48614bb6ec599f990a0d58bde
size 86022

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb1abcb74778fb7082811410777496acce38676af018a58af78bbbf68cfda464
size 150430

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c505527895a78b1f1ced45259e668701c7ed32c032ba7a522becc3e6e4530deb
size 126683

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:43315418672bc1cd7d6fb791e3fe5f4dfc1ecf49f0503183f3bc679fb5a413db
size 73438

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ddf5ac592a481defc683c867d4d2f64f49be843d690447377537c8ef9f8df3c6
size 102237

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:05a25085da5f7eddec3bccf11648253018fc6541e5a09363c36cdf753c7215ba
size 103501

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bc019d5c67817b9e9865a151dc6e9e82d642d68f6837372d86986ca42b0ebde1
size 135029

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a1f2e71acbdd905e58dbd69f0e7e05e431b3131e69c393fe1811c82d68ec6356
size 435109

493
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md

@ -0,0 +1,493 @@
---
title: "Dans les coulisses de l'Open Code Quest : comment j'ai conçu le Leaderboard"
date: 2024-11-05T00:00:00+02:00
#lastMod: 2024-10-11T00:00:00+02:00
opensource:
- Prometheus
- Grafana
topics:
- Observability
# Featured images for Social Media promotion (sorted from by priority)
images:
- counting-scheme-with-time.png
resources:
- '*.png'
- '*.svg'
- '*.gif'
---
Lors du {{< internalLink path="/speaking/red-hat-summit-connect-france-2024/index.md" >}}, j'ai animé un atelier pour les développeurs intitulé "**Open Code Quest**".
Dans cet atelier, les développeurs devaient coder des micro-services en utilisant Quarkus, OpenShift et un service d'Intelligence Artificielle : le modèle Granite d'IBM.
L'atelier était conçu sous la forme d'une compétition de vitesse : les premiers à valider les trois exercices ont reçu une récompense.
J'ai conçu et développé le **Leaderboard** qui affiche la progression des participants et les départage en fonction de leur rapidité.
Facile ?
Pas tant que ça car je me suis imposé une figure de style : utiliser **Prometheus** et **Grafana**.
Suivez-moi dans les coulisses de l'Open Code Quest : comment j'ai conçu le Leaderboard !
<!--more-->
## Description de l'atelier
L'atelier **Open Code Quest** a été conçu pour accueillir 96 participants devant réaliser **et valider** 3 exercices.
Valider la bonne réalisation d'un exercice n'implique pas de lire le code du participant : si le micro-service démarre et répond aux requêtes, c'est validé !
Il n'y a donc pas de dimension créative, c'est une course de vitesse et d'attention (il faut juste bien lire [l'énoncé](https://cescoffier.github.io/quarkus-openshift-workshop/)).
Le coeur de l'atelier est une application web de simulation de combat entre [super-héros](https://fr.wikipedia.org/wiki/Super-h%C3%A9ros) et [super-vilains](https://fr.wikipedia.org/wiki/Super-vilain).
Il y a trois exercices :
- Développer et déployer le micro-service "**hero**"
- Développer et déployer le micro-service "**villain**"
- Développer et déployer le micro-service "**fight**"
Pour plus de détails, je vous renvoie à l'[énoncé de l'atelier](https://cescoffier.github.io/quarkus-openshift-workshop/overview/).
## Besoins
Le **Leaderboard** doit permettre deux choses :
- **encourager les participants** en introduisant une dose de compétition
- **déterminer les 30 participants les plus rapides** pour leur remettre un prix
Dans les précédentes éditions de cet atelier, on validait la bonne réalisation sur la base de captures d'écran envoyées sur un channel Slack.
Les participants envoyaient les captures d'écran, l'animateur les validait dans l'ordre, notait les points dans une feuille Google Sheet et annonçait la progression à intervalle régulier.
Un animateur était dédié à la gestion du leaderboard.
Cette année, il était attendu que le processus soit **entièrement automatisé** pour éviter ces tâches administratives chronophages.
## Principe de fonctionnement
Je le disais en introduction, pour la réalisation de ce **Leaderboard** je me suis imposé une figure de style : utiliser **Prometheus** et **Grafana**.
Prometheus est une base de données **time series**.
C'est à dire qu'il est optimisé pour stocker l'évolution de données numériques au cours du temps et faire des statistiques sur ces données.
Grafana permet de présenter les données de Prometheus sous la forme de tableaux de bord.
Ces deux outils sont beaucoup utilisés dans deux produits que l'on a utilisés pour cet atelier : **Red Hat OpenShift Container Platform** et **Red Hat Advanced Cluster Management**.
Prometheus est très efficace pour savoir que "*le Pod X dans le namespace Y vient de passer à l'état Running*".
Et c'est justement ce qui nous intéresse :
- Si le Pod **hero-database-1** est créé dans le namespace **batman-workshop-prod** alors on sait que l'utilisateur **batman** vient de terminer le déploiement de la base de donnée de l'exercice **hero** dans l'environnement de **prod**.
- Si le Deployment **hero** dans le namespace **batman-workshop-prod** passe à l'état **Available**, alors on sait que l'utilisateur vient de déployer avec succès son micro-service **hero**.
- Si un Pod **batman-hero-run-*\<random>*-resync-pod** dans le namespace **batman-workshop-dev** passe à l'état **Completed**, alors on sait que le dernier pipeline Tekton l'utilisateur vient de terminer avec succès.
Et si les trois conditions précédentes sont vraies, on peut en déduire que l'utilisateur a terminé et validé l'exercice **hero**.
Au cours du temps, ces *time series* progressent telles que représentées sur la figure suivante.
{{< attachedFigure src="exercise-validation.png" title="Lorsque les trois conditions sont réunies, l'exercice est validé." >}}
C'est un bon début, non ?
Si on fait la même chose pour les trois exercices, on peut savoir qui a terminé l'atelier dans son ensemble.
Vu que certains exercices prennent plus de temps que d'autres, on peut imaginer attribuer plus de points aux exercices longs et moins aux exercices courts.
C'est ce que j'ai essayé de modéliser dans la figure ci-dessous avec un poids de 55 pour le premier exercice, 30 pour le second et 45 pour le dernier.
L'idée étant d'approcher une progression linéaire des points au cours du temps (1 point par minute).
{{< attachedFigure src="counting-scheme-no-time.png" title="Progression du nombre de points pour un utilisateur normal, lent et rapide au cours du temps et avec pondération de chaque exercise en fonction de la durée nominale de l'exercise." >}}
Ça commence à prendre forme.
Mais si on regarde bien, à la fin de l'atelier (à la 150ème minute), tous les participants ont terminé et ont le même score.
Et cela me pose deux problèmes :
- Pour commencer, **trier des participants par ordre d'arrivée, Prometheus ne sait pas faire**.
Et je n'ai pas envie, au moment de la remise des prix de devoir analyser les résultats minute par minute pour noter manuellement l'ordre d'arrivée des participants.
- Ensuite, si tous les participants ayant validé un exercice ont le même score, **où est le frisson de la compétition** ?
Je sais bien qu'avec n'importe quel base de données SQL on aurait juste à faire un `SELECT * FROM users ORDER BY ex3_completion_timestamp ASC` pour avoir le résultat.
Je sais bien que j'essaye d'utiliser Prometheus pour une tâche qui n'est pas vraiment la sienne.
Mais, soyons fous...
Rêvons deux minutes...
**Et si on essayait de contourner cette limitation de Prometheus ?**
Est-ce qu'on ne pourrait pas modérer ou accentuer la pondération d'un exercice en fonction du temps qu'a mis l'utilisateur à réaliser l'exercice ?
Est-ce qu'on ne pourrait pas activer un accélérateur à chaque validation d'un exercice qui donnerait quelques points en plus à chaque minute qui passe ?
Voilà qui rendrait la compétition plus engageante et plus amusante !
Et c'est ce que j'ai essayé de modéliser sur le schéma ci-dessous.
{{< attachedFigure src="counting-scheme-with-time.png" title="Progression du nombre de points pour un utilisateur normal, lent et rapide au cours du temps et avec accélérateur et pondération de chaque exercise en fonction du temps que met l'utilisateur à réaliser l'exercice." >}}
Maintenant, la question est : est-ce qu'un utilisateur qui prend la tête dans le premier exercice acquiert un avantage significatif qui rendrait la compétition déséquilibrée ?
La réponse, nous l'avons obtenue lors des différentes répétitions qui ont eu lieu chez Red Hat avant le Jour J.
{{< attachedFigure src="counting-scheme-dry-run.png" title="Validation du modèle de comptage des points lors d'un dry-run." >}}
Dans la capture d'écran ci-dessus, on voit que Batman a terminé l'exercice "hero" **tardivement**.
Mais en terminant l'exercice "villain" **très rapidement**, il a pu reprendre la tếte... **temporairement**.
Catwoman qui menait le jeu, lui repasse devant avant que Batman ne reprenne la tête et ne conserve son avance jusqu'au dernier moment.
Ouf ! Quel suspense !
Donc, **il est définitivement possible de partir en retard et de rattraper son retard.**
Le principe est validé !
Et maintenant, comment est-ce qu'on implémente ça dans Prometheus ?
## Implémentation dans Prometheus
Si j'avais dû mettre au point ce système de comptage des points dans un Prometheus pré-configuré pour de la production, j'aurais fait face à deux difficultés :
1. Par défaut, la résolution temporelle du couple Prometheus + Grafana inclus dans **Red Hat Advanced Cluster Management** est de 5 minutes (ça correspond au pas de temps minimum entre deux mesures).
Valider le bon comptage des points avec une résolution de 5 minutes sur un atlier de 2h30 prend 2h30 (**vitesse réelle**).
2. Pour implémenter ce système de comptage des points, j'ai besoin d'utiliser des *recording rules*.
Or, la modification d'une *recording rule* **ne déclenche pas automatiquement la réécriture des *time series* calculées dans le passé**.
Pour ces deux raisons, j'ai décidé de passer par un banc d'essai spécifique.
### Utilisation d'un banc d'essai
Les spécificités de ce banc d'essai sont les suivantes :
- La périodicité de *scrapping* de Prometheus est configurée à **5 secondes**.
Ainsi, valider le bon comptage des points se fait **60 fois plus vite**: 2h30 d'atelier se valide en 2m30, avec une résolution de 5 minutes.
- À chaque itération, le Prometheus est reconfiguré avec les nouvelles *recording rules*, les *times series* passées sont effacées et **Prometheus démarre immédiatement l'enregistrement des nouvelles *time series* à partir d'un jeu de données de test standardisé**.
La mise au point est donc grandement facilitée !
Le banc d'essai est disponible dans l'entrepôt Git [opencodequest-leaderboard](https://github.com/nmasse-itix/opencodequest-leaderboard) et ne nécessite que peu de pré-requis : `git`, `bash`, `podman`, `podman-compose` ainsi que la commande `envsubst`. Ces dépendances sont habituellement installable avec les paquets de votre distribution (`dnf install git bash podman podman-compose gettext-envsubst` sur Fedora).
Récupérez le code du banc d'essai et démarrez-le :
```sh
git clone https://github.com/nmasse-itix/opencodequest-leaderboard.git
cd opencodequest-leaderboard
./run.sh
```
Au premier démarrage, connectez-vous à l'interface de Grafana (`http://localhost:3000`) et réalisez ces 4 actions :
- S'authentifier avec le login **admin** et le mot de passe **admin**.
- Définir un nouveau mot de passe administrateur (ou juste cliquer sur **Skip**...)
- Configurer une source de données par défaut de type **Prometheus** avec les valeurs suivantes :
- **Prometheus server URL**: `http://prometheus:9090`
- **Scrape interval**: `5s`
- Créer un nouveau *dashboard* depuis le fichier **grafana/leaderboard.json** qui est dans l'entrepôt Git.
Des données doivent normalement apparaître dans le tableau de bord Grafana.
Pour en profiter pleinement, arrêtez le script `run.sh` avec un appui sur **Ctrl + C** et relancez le !
Au bout de quelques secondes, vous devriez voir apparaitre sur le tableau de bord des données toutes fraiches, comme dans la vidéo ci-dessous.
{{< attachedFigure src="leaderboard-simulation.gif" title="Simulation de l'atelier Open Code Quest sur le banc d'essai afin de valider le système de comptage de points (vidéo accélérée 10x)." >}}
### Requêtes Prometheus
Les requêtes Prometheus que j'ai utilisées sont stockées dans le fichier `prometheus/recording_rules.yaml.template`.
C'est un *template* qui contient des variables.
Ces variables sont remplacées par leur valeur lors de l'exécution du script `run.sh`.
Toutes les requêtes sont enregistrées sous la forme de *recording rules* Prometheus.
Elles sont réparties en trois groupes :
1. Les requêtes `opencodequest_leaderboard_*` représentent l'état de complétude d'un exercice par un utilisateur.
2. Les requêtes `opencodequest_leaderboard_*_onetime_bonus` représentent le bonus temps qu'acquiert un utilisateur qui termine un exercice.
3. Les requêtes `opencodequest_leaderboard_*_lifetime_bonus` représentent le report à nouveau du bonus temps qu'acquiert un utilisateur qui termine un exercice.
#### Requêtes `opencodequest_leaderboard_*`
Les trois requêtes qu'il faut comprendre en premier sont :
- `opencodequest_leaderboard_hero:prod` : état de complétude de l'exercice **hero** (0 = non terminé, 1 = terminé)
- `opencodequest_leaderboard_villain:prod` : état de complétude de l'exercice **villain** (*idem*)
- `opencodequest_leaderboard_fight:prod` : état de complétude de l'exercice **fight** (*idem*)
Ces trois requêtes sont conçues sur le même modèle.
J'ai pris la première que j'ai légèrement adaptée et formattée pour qu'elle soit plus compréhensible.
C'est presque une requète valide.
Il faudra juste, avant de l'exécuter, remplacer $EPOCHSECONDS par le *timestamp unix* de l'heure courante.
```
sum(
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "superman", "","")
) >= bool ($EPOCHSECONDS + 55)
or
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "catwoman", "","")
) >= bool ($EPOCHSECONDS + 50)
or
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "invisibleman", "","")
) >= bool ($EPOCHSECONDS + 60)
or
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "batman", "","")
) >= bool ($EPOCHSECONDS + 65)
) by (user)
```
Pour remplacer `$EPOCHSECONDS` par le *timestamp unix* de l'heure courante, vous pouvez passer par un *here-doc* dans votre Shell préféré :
```sh
cat << EOF
Requète Prometheus
EOF
```
Copiez-collez la requète dans la section **Explore** de Grafana et vous devriez obtenir le graphe suivant.
{{< attachedFigure src="grafana-explore-opencodequest-leaderboard-hero.png" title="La métrique \"opencodequest_leaderboard_hero:prod\" représente l'état de complétude de l'exercice \"hero\" dans l'environnement \"prod\"." >}}
Il faut le lire de la manière suivante (note : 1728646377 = 13:32:57) :
- **Superman** termine l'exercice hero **50 secondes** après le démarrage de l'atelier.
- **Catwoman** termine l'exercice hero **55 secondes** après le démarrage de l'atelier.
- **Invisible Man** termine l'exercice hero **60 secondes** après le démarrage de l'atelier.
- **Batman** termine l'exercice hero **65 secondes** après le démarrage de l'atelier.
Cette requête fonctionne de la manière suivante :
- `up{instance="localhost:9090"}` est une *time serie* qui retourne toujours **1**, accompagnée de plein de *labels* qui nous sont inutiles pour notre besoin.
- `label_replace(TIMESERIE, "user", "superman", "","")` ajoute l'étiquette **user=superman** à la *time serie*.
- `timestamp(TIMESERIE) >= bool TS` retourne **1** pour toute mesure prise **après** le timestamp TS, 0 sinon.
- `TIMESERIE1 or TIMESERIE2` fusionne les deux *time series*.
- `sum(TIMESERIE) by (user)` supprime toutes les étiquettes, sauf `user`.
J'aurais pu utiliser `min`, `max`, etc. à la place de `sum` car je n'ai qu'une seule timeserie par valeur de **user**.
Le résultat de ces trois requêtes est stocké dans Prometheus sous la forme de *time series* grace aux *recording rules* qui les définissent.
**Elles représentent le jeu de données de test qui me sert à valider le bon fonctionnement du Leaderboard**.
Dans l'environnement **Open Code Quest**, elles seront remplacées par des vraies métriques en provenance des *clusters* OpenShift.
#### Requêtes `opencodequest_leaderboard_*_onetime_bonus`
Les requêtes suivantes calculent un bonus temps pour les utilisateurs qui terminent un exercice.
Plus l'utilisateur termine tôt l'exercice (par rapport à l'heure de fin prévue), plus le bonus est conséquent.
Et inversement, plus l'utilisateur est en retard par rapport à l'heure de fin prévue, moins le bonus est conséquent.
- `opencodequest_leaderboard_hero_onetime_bonus:prod` représente le bonus temps affecté à l'utilisateur qui termine l'exercice **hero**.
- `opencodequest_leaderboard_villain_onetime_bonus:prod` représente le bonus temps affecté à l'utilisateur qui termine l'exercice **villain**.
- `opencodequest_leaderboard_fight_onetime_bonus:prod` représente le bonus temps affecté à l'utilisateur qui termine l'exercice **fight**.
Ces trois requêtes sont conçues sur le même modèle.
Ça peut paraître complexe de prime abord mais en fait pas tant que ça.
```
(increase(opencodequest_leaderboard_hero:prod[10s]) >= bool 0.5)
*
(
55
+
sum(
(
${TS_EXERCISE_HERO}
-
timestamp(
label_replace(up{instance="localhost:9090"}, "user", "superman", "","")
or
label_replace(up{instance="localhost:9090"}, "user", "invisibleman", "","")
or
label_replace(up{instance="localhost:9090"}, "user", "catwoman", "","")
or
label_replace(up{instance="localhost:9090"}, "user", "batman", "","")
)
) / 5
) by (user)
)
```
Pour bien comprendre comment fonctionne cette requête, je vous propose de la scinder en deux : la partie `increase(...)` d'un coté et le reste de l'autre.
On superpose tout ça avec la requête précédente et ça donne la figure suivante.
{{< attachedFigure src="grafana-opencodequest-leaderboard-onetime-bonus.png" title="La métrique \"opencodequest_leaderboard_hero_onetime_bonus:prod\" représente le bonus temps alloué à un utilisateur lorsqu'il termine l'exercice \"hero\" dans l'environnement \"prod\"." >}}
De haut en bas, on peut observer :
1. La requête `opencodequest_leaderboard_hero:prod`.
Elle représente l'état de complétude de l'exercice.
2. La partie `increase(opencodequest_leaderboard_hero:prod[10s]) >= bool 0.5` détecte les changements d'état de la requête précédente.
3. La partie `55 + sum(($TS - timestamp(...) / 5) by (user)` représente l'évolution du bonus temps au cours du temps.
Le terme **55** est le bonus nominal de l'exercice et le diviseur **5** permet de faire varier le bonus **d'une unité toutes les 5 secondes**.
4. Le tout est l'application du bonus temps au moment où l'utilisateur termine l'exercice.
#### Requêtes `opencodequest_leaderboard_*_lifetime_bonus`
Les requêtes suivantes reportent le bonus temps de mesures en mesures jusqu'à la fin de l'atelier.
- `opencodequest_leaderboard_hero_lifetime_bonus:prod` représente le report à nouveau du bonus temps affecté à l'utilisateur qui termine l'exercice **hero**.
- `opencodequest_leaderboard_villain_lifetime_bonus:prod` représente le report à nouveau du bonus temps affecté à l'utilisateur qui termine l'exercice **villain**.
- `opencodequest_leaderboard_fight_lifetime_bonus:prod` représente le report à nouveau du bonus temps affecté à l'utilisateur qui termine l'exercice **fight**.
Ces trois requêtes sont conçues sur le même modèle :
```
sum_over_time(opencodequest_leaderboard_hero_onetime_bonus:prod[1h])
```
La fonction `sum_over_time(TIMESERIE)` effectue la somme des valeurs de la *time serie* au cours du temps.
On peut le voir comme l'intégrale de la *time serie*.
La figure suivante présente le fonctionnement de cette requête de manière plus parlante.
{{< attachedFigure src="grafana-opencodequest-leaderboard-lifetime-bonus.png" title="La métrique \"opencodequest_leaderboard_hero_lifetime_bonus:prod\" représente le report à nouveau du bonus temps alloué à un utilisateur lorsqu'il termine l'exercice \"hero\" dans l'environnement \"prod\"." >}}
De haut en bas, on peut observer :
1. La requête `opencodequest_leaderboard_hero:prod`.
Elle représente l'état de complétude de l'exercice.
2. La requête `opencodequest_leaderboard_hero_onetime_bonus:prod`.
Elle représente l'application du bonus temps au moment où l'utilisateur termine l'exercice.
3. Le résultat est le report à nouveau du bonus temps depuis le moment où l'utilisateur termine l'exercice.
Note: on voit un décalage d'une unité de temps entre la dernière requête et les deux premières
Je pense que c'est une conséquence des dépendances entre les *recording rules*.
#### La requête finale
La requête finale qui détermine les points des utilisateurs est la somme de 6 composantes :
- Le bonus temps de l'exercice **hero** (reporté)
- L'accélérateur activé à la fin de l'exercice **hero**
- Le bonus temps de l'exercice **villain** (reporté)
- L'accélérateur activé à la fin de l'exercice **villain**
- Le bonus temps de l'exercice **fight** (reporté)
- L'accélérateur activé à la fin de l'exercice **fight**
Dans le dialecte utilisé par Prometheus, cela s'écrit de la façon suivante :
```
opencodequest_leaderboard_hero_lifetime_bonus:prod
+ sum_over_time(opencodequest_leaderboard_hero:prod[1h])
+ opencodequest_leaderboard_villain_lifetime_bonus:prod
+ sum_over_time(opencodequest_leaderboard_villain:prod[1h])
+ opencodequest_leaderboard_fight_lifetime_bonus:prod
+ sum_over_time(opencodequest_leaderboard_fight:prod[1h])
```
Les bonus temps ont été décrit dans la section précédente.
Il ne me reste donc qu'à vous expliquer le fonctionnement de l'accélérateur.
Les *time series* `opencodequest_leaderboard_{hero,villain,fight}:prod` sont l'état de complétude de l'exercice (valeur binaire : 0 ou 1).
Pour obtenir [une rampe](https://fr.wikipedia.org/wiki/Rampe_%28fonction%29), il faut prendre son intégrale.
J'utilise donc la fonction `sum_over_time(TIMESERIE)` à cet effet.
Pour corser le jeu, on pourrait imaginer changer la pente de la rampe via un coefficient multiplicateur mais j'ai jugé que ce n'était pas nécessaire.
En effet, les 3 accélérateurs s'additionnent déjà, ce qui fait que l'utilisateur gagne 1 point toutes les 5 minutes qui passent après l'exercice **hero**, 2 points après l'exercice **villain** et 3 points après l'exercice **fight**.
La figure suivante présente les 6 composantes de requête Prometheus permettant de calculer les points de l'utilisateur.
{{< attachedFigure src="grafana-opencodequest-leaderboard.png" title="Les 6 composantes de la requête Prometheus calculant les scores des utilisateurs et le résultat." >}}
### *Recording Rules*
Les requêtes `opencodequest_leaderboard_*` s'appuient sur la fonction **increase** et les requêtes `opencodequest_leaderboard_*_lifetime_bonus` s'appuient sur la fonction **sum_over_time**.
Ces deux fonctions Prometheus ont une contrainte : on ne peut les appliquer **que sur un *range vector*** (c'est la syntaxe `timeserie[range]` que vous avez aperçue dans les exemples ci-dessus).
Et **un *range vector* ne peut pas être le résultat d'un calcul**.
C'est à dire que la requête suivante est valide :
```cpp
// OK
sum_over_time(
opencodequest_leaderboard_hero:prod[1h]
)
```
Mais celles-ci ne le sont pas :
```cpp
// parse error: ranges only allowed for vector selectors
sum_over_time(
(1 + opencodequest_leaderboard_hero:prod)[1h]
)
// parse error: binary expression must contain only scalar and instant vector types
sum_over_time(
1 + opencodequest_leaderboard_hero:prod[1h]
)
```
Cela signifie qu'il n'est pas possible de construire une méga-requête qui calculerait le score de tous les participants au cours du temps.
Il faut donc, à chaque utilisation d'une de ces fonctions nécessitant un *range vector*, passer par une *recording rule* pour matérialiser le résultat du calcul dans une *time serie* nommée.
Et comme nos requêtes dépendent les unes des autres, il faut les placer dans des groupes de *recording rule* différents.
C'est pour cette raison que vous retrouverez dans le fichier `prometheus/recording_rules.yaml.template`, trois groupes de *recording rules* :
- `opencodequest_base` pour le jeu de données de test (qui n'existe que dans le banc d'essai).
- `opencodequest_step1` pour les requêtes `opencodequest_leaderboard_*_onetime_bonus`.
- `opencodequest_step2` pour les requêtes `opencodequest_leaderboard_*_lifetime_bonus`.
Et vous verrez dans l'article suivant que les *recording rules* dans une configuration **Red Hat Advanced Cluster Management** ont quelques subtilités...
## Création du tableau de bord Grafana
Une fois toutes les requêtes Prometheus mises au point, la création du tableau de bord Grafana est relativement simple :
- Créer deux variables : **env** (l'environnement des participants sur lequel calculer le score) et **user** (la liste des utilisateurs à inclure dans le leaderboard).
- Ajouter deux visualisations : une pour le classement instantané et une pour la progression des points au cours du temps.
La variable **user** est multi-valuée (on peut sélectionner tous les utilisateurs ou décocher les utilisateurs qu'on ne veut pas voir... comme ceux ayant servi à la recette la veille !) et les valeurs possibles sont extraites des *labels* d'une *time serie* Prometheus (peu importe laquelle, tant que tous les utilisateurs sont représentés).
La variable **env** a trois valeurs possibles ("dev", "preprod" ou "prod") mais on ne peut sélectionner qu'une valeur à la fois.
Ces deux variables s'utilisent ensuite dans la requète du Leaderboard de la manière suivante :
```
max(
opencodequest_leaderboard_hero_lifetime_bonus:${env:text}{user=~"${user:regex}"}
+ sum_over_time(opencodequest_leaderboard_hero:${env:text}{user=~"${user:regex}"}[1h])
+ opencodequest_leaderboard_villain_lifetime_bonus:${env:text}{user=~"${user:regex}"}
+ sum_over_time(opencodequest_leaderboard_villain:${env:text}{user=~"${user:regex}"}[1h])
+ opencodequest_leaderboard_fight_lifetime_bonus:${env:text}{user=~"${user:regex}"}
+ sum_over_time(opencodequest_leaderboard_fight:${env:text}{user=~"${user:regex}"}[1h])
) by (user)
```
La syntaxe `${user:regex}` permet à Grafana de remplacer `user=~"${user:regex}"` par `user=~"(batman|catwoman|invisibleman|superman)"` lorsque plusieurs valeurs sont sélectionnées dans la liste déroulante.
### Visualisation du classement instantané
Pour montrer le classement instantané, j'ai utilisé la visualisation **Bar Chart** avec une transformation de type **Sort by** sur le champ **Value**.
{{< attachedFigure src="grafana-opencodequest-leaderboard-instant-snapshot.png" title="Paramètres de la visualisation Grafana pour le classement instantané." >}}
Les paramètres importants de cette visualisation sont :
- **Format** : `Table`
- **Type** : `Instant`
- **Legend** : `{{user}}` (pour afficher le nom du participant en face de son score)
### Visualisation des points au cours du temps
Pour suivre la progression des points au cours du temps, j'ai opté pour la visualisation **Time series**.
{{< attachedFigure src="grafana-opencodequest-leaderboard-points-over-time.png" title="Paramètres de la visualisation Grafana pour la progression des points." >}}
Les paramètres importants de cette visualisation sont :
- **Format** : `Time series`
- **Type** : `Range`
- **Min step** : `5s` lors de la mise au point sur le banc d'essai et `5m` en vrai.
### Résultat
Le tableau de bord utilisé le jour de l'Open Code Quest était peu ou prou ce que l'on voit sur la figure 5 (le gif animé) :
- Le classement instanané, projeté par moment sur le vidéo projecteur pour annoncer les scores intermédiaires.
- La progression des points au cours du temps, affichée sur un deuxième écran pour garder un oeil sur la compétition.
Vous retrouverez tous les tableaux de bord Grafana présentés ici dans le dossier [grafana](https://github.com/nmasse-itix/opencodequest-leaderboard/tree/main/grafana).
## Le jour de l'Open Code Quest
Le jour de l'Open Code Quest, le Leaderboard a bien fonctionné et nous a permis de déterminer les 30 participants les plus rapides.
Ils sont montés sur scène pour recevoir une récompense.
Quant à la question qui est sur toutes les lèvres : est-ce qu'il y a eu de la baston entre super héros pour le podium ?
La réponse est un grand **OUI** !
Et il y a eu du frisson lors de l'annonce des résultats...
{{< attachedFigure src="grafana-opencodequest-points.png" title="Progression des points des 74 participants lors de l'Open Code Quest." >}}
Observez toutes ces courbes qui se croisent, tous ces super-héros en compétition pour la première place !
## Conclusion
En conclusion, l’Open Code Quest a été une expérience aussi stimulante pour les participants que pour moi en tant qu'organisateur.
Ce projet a non seulement mis en lumière des technologies comme Quarkus, OpenShift et le modèle Granite d’IBM, mais il a également démontré à quel point des outils comme Prometheus et Grafana peuvent être utilisés de manière créative pour répondre à des problématiques bien concrètes.
Concevoir le Leaderboard, bien que complexe, a ajouté une dimension compétitive motivante à l’atelier.
Le jour J, voir les participants rivaliser de rapidité tout en explorant les solutions Red Hat a été incroyablement gratifiant.
Et pour savoir comment j'ai implémenté ce Leaderboard dans une architecture multi-cluster avec Red Hat ACM, c'est par ici : {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md" >}}.

BIN
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

91
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-no-time.m

@ -0,0 +1,91 @@
# Defines a basic counting mechanism with no time bonus
exercise_timing = [ 55, 85, 130 ];
counting_params = [ 0, 0, 0; 55, 30, 45 ];
step = 5;
timeframe = [ 0, 150 ];
t = [ timeframe(1) : step : timeframe(2) ];
function y = count_basic (student_params, counting_params, step, timeframe)
# Timing variables
t0 = idivide(int32 (timeframe(1)), step);
t1 = idivide(int32 (student_params(1)), step);
t2 = idivide(int32 (student_params(2)), step);
t3 = idivide(int32 (student_params(3)), step);
tmax = idivide(int32 (timeframe(2)), step);
# Counting weights
p1 = counting_params(2, 1);
p2 = counting_params(2, 2);
p3 = counting_params(2, 3);
y = [ timeframe(1) : step : timeframe(2) ] * 0;
val = 0;
for i = 1:length(y)
tx = i - 1;
if (tx == t1)
val += p1;
elseif (tx == t2)
val += p2;
elseif (tx == t3)
val += p3;
else
endif
y(i) = val;
endfor
endfunction
# Unit tests
count_basic([55, 85, 130], counting_params, step, timeframe)
count_basic([50, 75, 115], counting_params, step, timeframe)
count_basic([60, 95, 145], counting_params, step, timeframe)
# Graph parameters
figure(1);
clf(1);
set(1, "defaulttextfontsize", 8);
set(1, "defaultaxesfontsize", 4);
xlabel("time");
ylabel("points");
title("Points awarded for each exercise without taking time into account");
xlim([0 150]);
ylim([0 150]);
# There will be multiple series on the same figure
hold on;
# End-of-exercise markers
line("xdata", [ 55, 55 ], "ydata", [ 0, 130 ], "linewidth", 2, "linestyle", "--", "color", "#777777");
text(55,0, "Expected end of \nexercise Hero ", "rotation", 90, "horizontalalignment", "right", "fontsize", 3, "fontunits", "points");
line("xdata", [ 85, 85 ], "ydata", [ 0, 130 ], "linewidth", 2, "linestyle", "--", "color", "#777777");
text(85,0, "Expected end of \nexercise Villain ", "rotation", 90, "horizontalalignment", "right", "fontsize", 3, "fontunits", "points");
line("xdata", [ 130, 130 ], "ydata", [ 0, 130 ], "linewidth", 2, "linestyle", "--", "color", "#777777");
text(130,0, "Expected end of \nexercise Fight ", "rotation", 90, "horizontalalignment", "right", "fontsize", 3, "fontunits", "points");
# Linear progression (for reference)
l1 = plot(t, t, "-;Linear progression: 1 point per minute;", "linewidth", 2);
# on-time user
y = count_basic([55, 85, 130], counting_params, step, timeframe);
l2 = plot(t, y, "-;normal user;", "linewidth", 4);
# early user
y = count_basic([50, 75, 115], counting_params, step, timeframe);
l3 = plot(t, y, "-;early user;", "linewidth", 4);
# late user
y = count_basic([60, 95, 145], counting_params, step, timeframe);
l4 = plot(t, y, "-;late user;", "linewidth", 4);
# Set axes line width
set(gca, "linewidth", 2)
# End of multiple series on the same figure
hold off;
# Legend
legend([l1, l2, l3, l4], "location", "northwest", "fontsize", 5, "fontunits", "points");
# Save figure as PNG file
print(gcf, "counting-scheme-no-time.tmp.png", "-dpng", "-S4096,2160");
# Add an alpha channel and remove the white background (requires GraphicsMagick)
system('gm convert counting-scheme-no-time.tmp.png -transparent white counting-scheme-no-time.png');

108
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-with-time.m

@ -0,0 +1,108 @@
# Expected end time (in minutes) for each exercise
exercise_timing = [ 55, 85, 130 ];
# Counting parameters for each of the 3 exercises
# - a1, a2, a3: time bonus for each set of 5 minutes passing
# - b1, b2, b3: time bonus for each completed exercise
# - r1, r2, r3: time reference for each exercise (counted in "set of 5 minutes")
# - z1, z2, z3: time penalty for each set of 5 minutes late according to the reference time rX for the exercise
counting_params = { 1, 2, 3, 55, 25, 29, num2cell(exercise_timing / 5){:}, 1, 2, 3 };
# Time resolution (5 minutes)
step = 5;
# The timeframe to graph (2h30m)
timeframe = [ 0, 150 ];
# Time vector
t = [ timeframe(1) : step : timeframe(2) ];
# Defines a counting mechanism with a time bonus
function y = count_with_time (student_params, counting_params, step, timeframe)
# Timing variables
[ t0, t1, t2, t3, tmax ] = num2cell(idivide(int32 ([ timeframe(1), student_params, timeframe(2) ]), step)){:};
# Counting weights
a0 = 0;
[a1, a2, a3, b1, b2, b3, r1, r2, r3, z1, z2, z3] = counting_params{:};
y = [ timeframe(1) : step : timeframe(2) ] * 0;
val = 0;
for i = 1:length(y)
tx = i - 1;
if (tx == t1)
val += b1 + (r1 - tx) * z1;
elseif (tx == t2)
val += b2 + (r2 - tx) * z2;
elseif (tx == t3)
val += b3 + (r3 - tx) * z3;
elseif (tx > t3)
val += a3;
elseif (tx > t2)
val += a2;
elseif (tx > t1)
val += a1;
elseif (tx > t0)
val += a0;
else
endif
y(i) = val;
endfor
endfunction
# Unit tests
y = count_with_time([55, 85, 130], counting_params, step, timeframe)
y = count_with_time([50, 75, 115], counting_params, step, timeframe)
y = count_with_time([60, 95, 145], counting_params, step, timeframe)
# Graph parameters
figure(1);
clf(1);
set(1, "defaulttextfontsize", 8);
set(1, "defaultaxesfontsize", 4);
xlabel("time");
ylabel("points");
title("Points awarded for each exercise with both a time bonus and an accelerator");
xlim([0 150]);
ylim([0 165]);
# There will be multiple series on the same figure
hold on;
# End-of-exercise markers
line("xdata", [ 55, 55 ], "ydata", [ 0, 165 ], "linewidth", 2, "linestyle", "--", "color", "#777777");
text(55,0, "Expected end of \nexercise Hero ", "rotation", 90, "horizontalalignment", "right", "fontsize", 3, "fontunits", "points");
line("xdata", [ 85, 85 ], "ydata", [ 0, 165 ], "linewidth", 2, "linestyle", "--", "color", "#777777");
text(85,0, "Expected end of \nexercise Villain ", "rotation", 90, "horizontalalignment", "right", "fontsize", 3, "fontunits", "points");
line("xdata", [ 130, 130 ], "ydata", [ 0, 165 ], "linewidth", 2, "linestyle", "--", "color", "#777777");
text(130,0, "Expected end of \nexercise Fight ", "rotation", 90, "horizontalalignment", "right", "fontsize", 3, "fontunits", "points");
# Linear progression (for reference)
l1 = plot(t, t, "-;Linear progression: 1 point per minute;", "linewidth", 2);
# on-time user
y = count_with_time([55, 85, 130], counting_params, step, timeframe);
l2 = plot (t, y, "-;normal user;", "linewidth", 4);
# early user
y = count_with_time([50, 75, 115], counting_params, step, timeframe);
l3 = plot (t, y, "-;early user;", "linewidth", 4);
# late user
y = count_with_time([60, 95, 145], counting_params, step, timeframe);
l4 = plot (t, y, "-;late user;", "linewidth", 4);
# Set axes line width
set(gca, "linewidth", 2)
# End of multiple series on the same figure
hold off;
# Legende
legend([l1, l2, l3, l4], "location", "northwest", "fontsize", 5, "fontunits", "points");
# Save figure as PNG file
print(gcf, "counting-scheme-with-time.tmp.png", "-dpng", "-S4096,2160");
# Add an alpha channel and remove the white background (requires GraphicsMagick)
system('gm convert counting-scheme-with-time.tmp.png -transparent white counting-scheme-with-time.png');

65
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/exercise-validation.m

@ -0,0 +1,65 @@
# Data to plot
t = [ 1 : 1 : 10 ];
service_deployed = [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1 ];
db_deployed = [ 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 ];
pipeline_finished = [ 0, 0, 0, 0, 1, 1, 1, 1, 1, 1 ];
exercise_completed = [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1 ];
# Logic timing diagram
figure(1);
clf(1);
# Plot 1
subplot(4, 1, 1);
set(1, "defaulttextfontsize", 8);
set(1, "defaultaxesfontsize", 4);
stairs(t, db_deployed, "-", "linewidth", 4);
axis("on", "tight");
xticks([0]);
yticks([0 1]);
ylabel("state");
title("Pod named 'hero-database-1' in state 'Running'");
set(gca, "linewidth", 2)
# Plot 2
subplot (4, 1, 2);
set(1, "defaulttextfontsize", 8);
set(1, "defaultaxesfontsize", 4);
stairs(t, pipeline_finished, "-", "linewidth", 4);
axis("on", "tight");
xticks([0]);
yticks([0 1]);
ylabel("state");
title("Tekton pipeline named 'hero' in state 'Completed'");
set(gca, "linewidth", 2)
# Plot 3
subplot(4, 1, 3);
set(1, "defaulttextfontsize", 8);
set(1, "defaultaxesfontsize", 4);
stairs(t, service_deployed, "-", "linewidth", 4);
axis("on", "tight");
xticks([0]);
yticks([0 1]);
ylabel("state");
title("Deployment named 'hero' in state 'Available'");
set(gca, "linewidth", 2)
# Plot 4
subplot(4, 1, 4);
set(1, "defaulttextfontsize", 8);
set(1, "defaultaxesfontsize", 4);
stairs(t, exercise_completed, "-", "linewidth", 4);
axis("on", "tight");
xticks([0]);
yticks([0 1]);
xlabel("time");
ylabel("state");
title("Exercise 'hero' completed");
set(gca, "linewidth", 2)
# Save figure as PNG file
print(gcf, "exercise-validation.tmp.png", "-dpng", "-S4096,2160");
# Add an alpha channel and remove the white background (requires GraphicsMagick)
system('gm convert exercise-validation.tmp.png -transparent white exercise-validation.png');

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-query.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1e3568508fded959a4b122d562a0d2291e7e685f9925c793570976b853a644c6
size 108597

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-transform.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:696ad63a0fc070e65c67645900042783ac6d737dff22b507293f449ad9ac387b
size 19420

3
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/leaderboard-simulation.mkv

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df3633bd995d679aa9292e02348e4133406bc64033cac2af5b3b5cda80548c29
size 6436622

6
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/stack.sh

@ -0,0 +1,6 @@
#!/bin/bash
set -Eeuo pipefail
magick grafana-leaderboard-instant-snapshot-{query,transform}.png -append ../grafana-opencodequest-leaderboard-instant-snapshot.png

12
content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/to-gif.sh

@ -0,0 +1,12 @@
#!/bin/sh
# Source and destination
src="leaderboard-simulation.mkv"
dest="../$(basename "$src" .mkv).gif"
tmp="/tmp/$(basename "$src" .mkv)-fast.mkv"
# Extract a part of the video and speed up playback by 10x
ffmpeg -y -ss 00:00:23.000 -i "$src" -to 00:02:50.000 -filter:v "setpts=0.1*PTS" -an "$tmp"
# Convert to a 720p GIF with infinite loop
ffmpeg -y -i "$tmp" -vf "fps=2,scale=-1:720:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 "$dest"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save