diff --git a/.gitattributes b/.gitattributes index d742373..3845d6e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,3 +3,6 @@ *.jpg filter=lfs diff=lfs merge=lfs -text *.rnote 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 diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png new file mode 120000 index 0000000..1a7b4e6 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png new file mode 120000 index 0000000..6464a19 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png new file mode 120000 index 0000000..150772c --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png new file mode 120000 index 0000000..ad82c86 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png new file mode 120000 index 0000000..4a8bdab --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png new file mode 120000 index 0000000..06d1870 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png new file mode 120000 index 0000000..2411e67 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png new file mode 120000 index 0000000..98e5f57 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png new file mode 120000 index 0000000..b95bd6d --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png new file mode 120000 index 0000000..7fa942c --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png new file mode 120000 index 0000000..ce57a2e --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md new file mode 100644 index 0000000..cb97c07 --- /dev/null +++ b/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! + + + +## 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-*\*-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" >}}. diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif new file mode 120000 index 0000000..3b53cf0 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md new file mode 100644 index 0000000..939462d --- /dev/null +++ b/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! + + + +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-*\*-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 `-hero-run--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 - <}}. + +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. diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png new file mode 120000 index 0000000..63a6581 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg b/content/english/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg new file mode 120000 index 0000000..3f7d8cd --- /dev/null +++ b/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 \ No newline at end of file diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png new file mode 100644 index 0000000..ce9b867 Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-dry-run.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png new file mode 100644 index 0000000..3d7888a Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-no-time.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png new file mode 100644 index 0000000..0aa76b7 Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/counting-scheme-with-time.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png new file mode 100644 index 0000000..f2279cd Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/exercise-validation.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png new file mode 100644 index 0000000..46cf903 Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-explore-opencodequest-leaderboard-hero.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png new file mode 100644 index 0000000..ba7d3ef Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-instant-snapshot.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png new file mode 100644 index 0000000..b81e17a Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-lifetime-bonus.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png new file mode 100644 index 0000000..4c66a0a Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-onetime-bonus.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png new file mode 100644 index 0000000..5ca44ba Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard-points-over-time.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png new file mode 100644 index 0000000..ef21132 Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-leaderboard.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png new file mode 100644 index 0000000..136e2f0 Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/grafana-opencodequest-points.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md new file mode 100644 index 0000000..52dd83b --- /dev/null +++ b/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 ! + + + +## 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-*\*-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" >}}. diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif new file mode 100644 index 0000000..a36fb9c Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/leaderboard-simulation.gif differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-no-time.m b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-no-time.m new file mode 100644 index 0000000..83be504 --- /dev/null +++ b/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'); diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-with-time.m b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/counting-scheme-with-time.m new file mode 100644 index 0000000..918e55a --- /dev/null +++ b/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'); diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/exercise-validation.m b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/exercise-validation.m new file mode 100644 index 0000000..c6efe4a --- /dev/null +++ b/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'); diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-query.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-query.png new file mode 100644 index 0000000..8e58d37 Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-query.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-transform.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-transform.png new file mode 100644 index 0000000..9fd587e Binary files /dev/null and b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/grafana-leaderboard-instant-snapshot-transform.png differ diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/leaderboard-simulation.mkv b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/leaderboard-simulation.mkv new file mode 100644 index 0000000..7a631db --- /dev/null +++ b/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 diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/stack.sh b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/stack.sh new file mode 100755 index 0000000..9f30a69 --- /dev/null +++ b/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 + diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/to-gif.sh b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/sources/to-gif.sh new file mode 100755 index 0000000..05fddad --- /dev/null +++ b/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" diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md new file mode 100644 index 0000000..4bc34d0 --- /dev/null +++ b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/index.md @@ -0,0 +1,453 @@ +--- +title: "Dans les coulisses de l'Open Code Quest : comment j'ai implémenté le Leaderboard dans 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' +--- + +Après avoir révélé les coulisses de la conception du Leaderboard pour l'atelier "Open Code Quest" lors du {{< internalLink path="/speaking/red-hat-summit-connect-france-2024/index.md" >}}, il est temps de plonger plus en détail dans son implémentation pratique ! + +Dans cet article, je vais vous guider à travers la configuration de **Red Hat Advanced Cluster Management** ainsi que les différentes adapatations nécessaires pour connecter le *Leaderboard* créé précédemment avec l'infrastructure de l'**Open Code Quest**. + +Embarquez avec moi pour cette nouvelle étape, plus technique que la précédente, j'ai dû faire preuve de créativité pour câbler un tableau de bord Grafana très "conceptuel" avec la réalité des clusters OpenShift ! + + + +Cet article est la suite de {{< internalLink path="/blog/behind-the-scenes-at-open-code-quest-how-i-designed-leaderboard/index.md" >}}. +Si vous ne l'avez pas lu, je vous conseille de le lire avant pour mieux comprendre le contexte. + +## Requêtes Prometheus + +Dans l'article précédent, j'avais évoqué a manière dont ont pouvait détecter les actions d'un utilisateur dans son environnement : + +- 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-*\*-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. + +Si les trois conditions précédentes sont vraies, on peut en déduire que l'utilisateur a terminé et validé l'exercice **hero**. + +La réalité est en fait un peu plus compliquée car entre le *Leaderboard* de l'article précédent, très conceptuel et ces éléments très techniques, il a fallu faire pas mal d'adaptation. + +Au final, pour chaque exercice j'ai eu à implémenter trois requêtes Prometheus pour détecter les trois conditions ci-dessus. +Fort heureusement, les trois exercices sont sur le même modèle donc le jeu de requêtes est très similaire pour les trois exercices. + +### Détection du micro-service Quarkus + +Je détecte le déploiement du micro-service Quarkus **hero** dans l'environnement de **dev** à l'aide de la requête suivante que je persiste sous la forme d'une *recording rule* nommée **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) +``` + +Cette requête est en deux parties. +La première partie fonctionne de la manière suivante : + +- `kube_deployment_status_condition{namespace=~"[a-zA-Z0-9]+-workshop-dev",deployment="hero",condition="Available",status="true"}` retourne le nombre de **Deployment** kubernetes ayant le nom **hero**, dans un namespace se terminant par **-workshop-dev** et étant dans un état **Available**. +- `label_replace(TIMESERIE, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-dev")` extrait le nom de l'utilisateur depuis le *label* **namespace** à l'aide d'une expression régulière et le stocke dans un *label* **user**. +- `sum(TIMESERIE) by (user)` supprime toutes les étiquettes sauf **user** (j'aurais pu utiliser les fonctions `min`, `max`, etc, ça marche aussi). +- `clamp_max(TIMESERIE, 1)` plafonne le résultat à 1 pour garantir que le résultat est binaire. + +Cette première partie retourne l'état du micro-service Quarkus **dès lors que le Deployment kubernetes existe**. +Tant que le Deployment kubernetes n'existe pas, aucune donnée n'est retournée par cette partie de la requête. + +Et la deuxième partie de la requête est là pour palier à ce problème : + +- `kube_namespace_status_phase{namespace=~"[a-zA-Z0-9]+-workshop-(dev|preprod|prod)",phase="Active"}` retourne les namespaces des participants étant dans un état actif (ils le sont tous durant la durée de l'atelier). +- `label_replace(TIMESERIE, "user", "$1", "namespace", "([a-zA-Z0-9]+)-workshop-(dev|preprod|prod)")` extrait le nom de l'utilisateur depuis le *label* **namespace** à l'aide d'une expression régulière et le stocke dans un *label* **user**. +- `sum(TIMESERIE) by (user)` supprime toutes les étiquettes sauf **user** (j'aurais pu utiliser les fonctions `min`, `max`, etc, ça marche aussi). +- `clamp(TIMESERIE, 0, 0)` force toutes les valeurs de la *time serie* à 0. + +Cette deuxième partie permet d'avoir une valeur par défaut (0) pour l'ensemble des participants, même lorsque les Deployment kubernetes ne sont pas encore présents. + +Le mot clé `or` au milieu des deux requêtes permet de fusionner les deux parties, la première ayant la priorité sur la seconde. + +Les micro-services **villain** et **fight**, ainsi que les environnements de **preprod** et **prod** sont sur le même principe. +Au total, ce sont 9 *time series* qui sont enregistrées sous la forme de *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` + +### Détection de la base de données + +Je détecte le déploiement de la base de données **hero** dans l'environnement de **dev** à l'aide de la requête suivante que je persiste sous la forme d'une *recording rule* nommée **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) +``` + +La requête est très similaire à la précédente, excepté que je me base sur l'état du **Pod** nommé **hero-database-1**. +C'est pour cette raison que j'utilise la timeserie **kube_pod_status_phase**. + +Le micro-service **villain**, ainsi que les environnements de **preprod** et **prod** sont sur le même principe. +Au total, ce sont 6 *time series* qui sont enregistrées sous la forme de *recording rules* (**fight** n'a pas de base de données) : + +- `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` + +Les *recording rules* de l'environnement **prod** sont un peu différentes car dans cet environnement la base de données est mutualisée entre tous les participants et déployée avant le démarrage de l'atelier avec le reste de l'infrastructure. +Par conséquent, je force la valeur des *time series* `opencodequest_hero_db_pod:prod` et `opencodequest_villain_db_pod:prod` à 1 en utilisant une variante de la deuxième partie de la requête expliquée plus haut : + +``` +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) +``` + +### Détection de la fin du Pipeline Tekton + +Détecter la fin du pipeline Tekton m'a demandé plus de travail car il n'existe pas de métrique standard pour connaître l'état d'un Pipeline. +Je me suis donc basé sur la présence d'un pod `-hero-run--resync-pod` dans l'environnement **dev** de l'utilisateur. +Ce Pod correspond à la dernière étape du Pipeline Tekton. +Donc si ce pod est dans un état **Completed**, c'est que le Pipeline s'est terminé avec succès. + +Je détecte l'état du Pipeline Tekton **hero** dans l'environnement de **dev** à l'aide de la requête suivante que je persiste sous la forme d'une *recording rule* nommée **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) +``` + +La requête est très similaire à la précédente, excepté que l'état attendu du Pod est différent (**Completed**) et le nom du Pod est différent. + +Les micro-services **villain** et **fight** sont sur le même principe. +Au total, ce sont 3 *time series* qui sont enregistrées sous la forme de *recording rules* (les pipelines n'existent que dans l'environnement **dev**) : + +- `opencodequest_hero_pipeline` +- `opencodequest_villain_pipeline` +- `opencodequest_fight_pipeline` + +### Détection de la fin de l'exercice + +Pour détecter la fin de l'exercice **hero** dans l'environnement de **dev**, je combine le résultat des trois requêtes précédentes à l'aide de la requète suivante que je persiste sous la forme d'une *recording rule* nommée **opencodequest_leaderboard_hero:dev**. + +``` +max( + (opencodequest_hero_quarkus_pod:dev + opencodequest_hero_db_pod:dev + opencodequest_hero_pipeline) == bool 3 +) by (user, cluster) +``` + +Cette requête fonctionne de la manière suivante : + +- `(opencodequest_hero_quarkus_pod:dev + opencodequest_hero_db_pod:dev + opencodequest_hero_pipeline) == bool 3` retourne 1 quand les trois composantes de l'exercice sont validées, 0 sinon. + L'opérateur **bool** est important car sans lui, la requête ne retournerait aucun résultat tant que les trois composantes de l'exercice ne sont pas validées. +- `max(TIMESERIE) by (user, cluster)` élimine tous les *labels* sauf **cluster** et **user**. + Ici, l'utilisation de la fonction `max` est intéressante pour conserver le niveau maximum de complétude de l'exercice si par exemple l'utilisateur a commencé l'exercice sur un cluster et l'a refait et terminé sur un autre cluster. + C'est un cas qui ne doit pas arriver mais dans le doute... + +L'exercice **fight** n'a que deux composantes car il n'a pas de base de données. +Les requêtes le concernant seront donc plus simples : + +``` +max( + (opencodequest_fight_quarkus_pod:prod + opencodequest_fight_pipeline) == bool 2 +) by (user, cluster) +``` + +C'est un total de 9 *recording rules* qui enregistrent l'état de complétude des 3 exercices au travers des 3 environnements des 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` + +Et avec ces dernières *recording rules* nous venons de raccorder le Leaderboard avec les environnements OpenShift utilisés pour l'Open Code Quest. +Voyons maintenant comment l'observabilité a été implémentée dans **Red Hat Advanced Cluster Management** ! + +## Observabilité dans Red Hat Advanced Cluster Management + +Lors de l'Open Code Quest, nous avions à notre disposition 8 clusters : + +- 1 cluster **central** +- 1 cluster pour l'intelligence artificielle +- 6 clusters répartis entre les participants (on avait prévu un cluster par table) + +**Red Hat Advanced Cluster Management** est installé sur le cluster **central** et à partir de là, il contrôle l'ensemble des clusters. + +L'observabilité est un module supplémentaire (dans le sens où il n'est pas installé par défaut) de **Red Hat Advanced Cluster Management** et ce module est basé sur les composants Open Source **Prometheus**, **Thanos** et **Grafana**. + +Le schéma suivant présente l'architecture du module d'observabilité dans **Red Hat Advanced Cluster Management**. +Je l'ai créé en observant les relations entre les composants à partir d'une installation d'ACM en version 2.11. + +{{< attachedFigure src="redhat-acm-observability-architecture.svg" title="Architecture logique de l'observabilité dans Red Hat Advanced Cluster Management 2.11" >}} + +Les composants déployés sur le cluster central sont en **vert**, ceux déployés sur les clusters managés sont en **bleu** et les éléments de configuration en **gris**. +J'ai aussi illustré les deux endroits possibles pour le calcul des *recording rules*, en **jaune**. + +On notera que les ConfigMap sur les clusters managés peuvent être déployées automatiquement depuis le cluster **central** via un **ManifestWork**. + +### Implémentation des *Recording rules* + +Les recording rules peuvent être calculées à deux moments différents : + +- Dans chaque cluster managé, avant envoi sur le cluster central. +- Dans le cluster central, après réception. + +Mais il y a une petite subtilité : ce choix est vrai pour les métriques standard d'OpenShift. + +Les *recording rules* faisant appel à des métriques *custom* (ie. le **User Workload Monitoring**) ne sont calculées **qu'après réception sur le cluster central**. +Il n'est pas possible de les calculer avant envoi sur le cluster central. +On peut uniquement spécifier des métriques *custom* à envoyer telles quelles. + +Elles ne se configurent pas non plus au même endroit en fonction de si c'est une métrique *custom* ou une métrique standard et de si c'est fait avant ou après envoi. + +Pour vous aider, j'ai fait un tableau récapitulatif : + +| Type de métrique | Calcul de la *recording rule* | Emplacement de la configuration | Nom de la ConfigMap | Clé | +| -------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------- | ----------------------- | +| standard | avant envoi | *namespace* `open-cluster-management-observability` sur le cluster **central** ou les clusters managés | `observability-metrics-custom-allowlist` | `metrics_list.yaml` | +| *custom* | **pas de calcul**, envoi tel quel | *namespace* `open-cluster-management-observability` sur le cluster **central** ou les clusters managés | `observability-metrics-custom-allowlist` | `uwl_metrics_list.yaml` | +| standard ou *custom* | à réception | *namespace* `open-cluster-management-observability` sur le cluster **central** | `thanos-ruler-custom-rules` | `custom_rules.yaml` | + +#### Calcul des *Recording Rules* avant envoi + +Pour l'envoi des métriques et le calcul des *recording rules* **avant envoi** sur le cluster **central**, ça se configure dans le *namespace* `open-cluster-management-observability` sur le cluster **central** via une 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\"} +``` + +La configuration ci-dessus permet de : + +- Envoyer la métrique *custom* `fights_total` telle quelle. +- Envoyer les métriques standard `kube_deployment_status_replicas_ready`, `kube_pod_status_phase` et `kube_namespace_status_phase` telles quelles. +- Créer une métrique `opencodequest_hero_quarkus_pod:dev` à partir de la requête Prometheus `kube_deployment_status_condition{...}` et envoyer le résultat. + +Lorsque cette ConfigMap est créée sur le cluster **central**, elle est automatiquement répliquée sur tous les clusters managés. +D'après la documentation, il est aussi possible de la créer dans chaque cluster managé pour personnaliser la configuration par cluster. + +#### Calcul des *Recording Rules* à réception + +Pour le calcul des *recording rules* **à réception** sur le cluster **central**, ça se configure aussi dans le *namespace* `open-cluster-management-observability` sur le cluster **central** mais via une autre 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"} +``` + +On notera que les syntaxes des deux ConfigMap ne sont pas identiques. + +- Dans la ConfigMap `observability-metrics-custom-allowlist`, les *double quotes* doivent être échappées, par un *backslash*. + Ce n'est pas le cas dans l'autre ConfigMap. +- La syntaxe de la ConfigMap `thanos-ruler-custom-rules` permet de spécifier des groupes de *recording rules* alors que l'autre ConfigMap ne permet pas de le faire. + +Note: les noms des métriques dans les exemples ci-dessus sont plus ou moins fictifs. +Ce ne sont pas ces configurations que j'ai utilisées au final. + +#### Choix d'implémentation + +J'ai choisi de calculer sous la forme de *recording rules* **dans les clusters managés**, les trois composantes permettant de valider la complétude d'un exercice, à savoir : + +- Le **Deployment** du micro-service Quarkus est dans l'état **Available**. +- Le **Pod** de la base de donnée, lorsqu'il y en a une, est présent et dans un état **Ready**. +- Le Pipeline Tekton du micro-service s'est terminé avec succès. + Comme il n'existe pas de métrique standard pour les Pipelines Tekton, la *recording rule* détecte la présence du **Pod** correspondant à la dernière étape du Pipeline et vérifie qu'il est dans un état **Completed**. + +J'ai créé ces *recording rules* pour les environnements de **dev**, **preprod** et **prod** des participants. +Ainsi, si le jour de l'Open Code Quest on avait eu un problème généralisé dans l'environnement **prod**, on aurait pu rapidement basculer le calcul des points sur un autre environnement en amont. + +Je vois un avantage à cette approche : calculer les trois composantes de chaque exercice dans les clusters managés permet de ne pas remonter trop de métriques au niveau du cluster **central**. + +À l'inverse, j'ai dû calculer sous la forme de *recording rules* au niveau du **cluster central** les requêtes Prometheus du Leaderboard décrites en première partie de cet article. +Effectivement, je n'ai pas trop eu le choix : j'avais besoin d'avoir plusieurs groupes de *recording rules* et cette fonction n'est disponible que dans la ConfigMap qui configure les *recording rules* du cluster **central**. + +Vous pouvez retrouver l'ensemble des *recording rules* utilisées pour l'Open Code Quest dans le dossier [acm](https://github.com/nmasse-itix/opencodequest-leaderboard/tree/main/acm). + +### Mise en place de l'observabilité + +Le déployement du module d'observabilité sur le cluster **central**, se fait très simplement en suivant [la documentation](https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.11/html/observability/observing-environments-intro#enabling-observability-service) : + +- Créer le namespace `open-cluster-management-observability`. +- Créer le *pull secret* permettant de télécharger les images sur **registry.redhat.io**. +- Créer un *bucket* S3. +- Créer la *Custom Resource Definition* `MultiClusterObservability`. + +Pour effectuer ces opérations, j'ai utiliser les commandes suivantes : + +```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 - <}}. + +Et enfin, exporter le tableau de bord sous la forme d'une ConfigMap. + +```sh +./generate-dashboard-configmap-yaml.sh "Red Hat Summit Connect 2024" +``` + +Un fichier `red-hat-summit-connect-2024.yaml` est créé. +Il suffit de l'appliquer sur le cluster **central** pour que le tableau de bord apparaisse dans l'instance Grafana de production. + +```sh +oc apply -f red-hat-summit-connect-2024.yaml +``` + +## Conclusion + +Pour conclure, l'implémentation du Leaderboard dans Red Hat Advanced Cluster Management m'a permis de mieux comprendre le fonctionnement de l'observabilité, en particulier les *recording rules*. +Au final, j'ai réussi à mettre en place un tableau de bord qui suit en temps réel l'avancée des participants. + +Retrouvez l'ensemble des *recording rules* utilisées pour l'Open Code Quest dans le dossier [acm](https://github.com/nmasse-itix/opencodequest-leaderboard/tree/main/acm) de l'entrepôt Git. diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png new file mode 100644 index 0000000..66c5512 --- /dev/null +++ b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e8f430bfd29849f2b21b583a181b3d070b3f6bf6d4da618c414c1ce58a8dc86 +size 84273 diff --git a/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg new file mode 100644 index 0000000..aab5c2f --- /dev/null +++ b/content/french/blog/behind-the-scenes-at-open-code-quest-how-i-implemented-leaderboard-with-acm/redhat-acm-observability-architecture.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/index.md b/content/french/speaking/red-hat-summit-connect-france-2024/index.md new file mode 100644 index 0000000..a28f4d8 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/index.md @@ -0,0 +1,160 @@ +--- +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 +--- + +Le 8 Octobre 2024, j'ai participé au [Red Hat Summit Connect France 2024](https://www.redhat.com/fr/summit/connect/emea/paris-2024) à double titre : + +- Je me suis occupé du Leaderboard de l'atelier **Open Code Quest** et j'ai assuré le rôle de SRE pour la plateforme de cet atelier. +- J'étais présent sur le stand RHEL pour présenter notre démo "Mission Impossible" avec le train Lego. + + + +## Open Code Quest : Une aventure technologique et héroïque + +L'atelier **Open Code Quest** a rassemblé des passionnés de technologie dans un cadre immersif où se mêlaient innovation technologique et univers de super-héros. +L'objectif était d'offrir aux participants une découverte approfondie de Quarkus, OpenShift, OpenShift AI, saupoudrés d'une pincée de sécurité et avec une expérience développeur sans couture. +Le tout en les plongeant dans une aventure captivante où chaque exercice impliquait des super-héros. + +Lors de cet atelier, les participants devaient développer pas moins de quatre micro-services pour construire une application de simulation de combat entre super héros et super villains. + +{{< attachedFigure src="open-code-quest-microservices.png" >}} + +Les micro-services ont été développés en Quarkus, le framework Java natif pour le cloud, en démontrant comment il peut transformer le développement d’applications en alliant rapidité de développement, légèreté et performance. +En particulier, Quarkus réduit considérablement l'empreinte mémoire des applications, tout en permettant un démarrage quasi instantané. + +Nous avons également positionné en tête de pont [Red Hat Developer Hub](https://developers.redhat.com/rhdh/overview), la distribution Red Hat de **Backstage**, une plateforme open source développée par Spotify pour améliorer la gestion des environnements complexes. +**Red Hat Developer Hub** a captivé l'attention des participants en offrant une interface unifiée pour centraliser la gestion des microservices, pipelines CI/CD et autres outils essentiels au développement. +Son extensibilité a permis d'intégrer facilement des plugins adaptés aux besoins de l'atelier, simplifiant ainsi le cycle de vie des applications. +Pour les développeurs comme pour les architectes, **Red Hat Developer Hub** s'est révélé être un outil précieux, facilitant la collaboration et apportant une vision claire de l'infrastructure tout en améliorant la productivité. + +Lors de l'**Open Code Quest**, nous avons également mis en lumière [Red Hat Trusted Application Pipelines](https://www.redhat.com/en/products/trusted-application-pipeline), un produit conçu pour sécuriser et automatiser la chaîne de construction des applications. +Basé sur les technologies **Tekton Chains** et **Sigstore**, ce produit offre une traçabilité complète et garantit l'intégrité des composants logiciels à chaque étape du pipeline CI/CD. +Les participants ont pu découvrir comment ces outils permettent de renforcer la sécurité des déploiements en fournissant des preuves de conformité et en assurant la transparence sur les dépendances utilisées dans les applications. + +Je vous laisse découvrir la liste complète de l'outillage utilisé dans l'atelier **Open Code Quest** : + +{{< attachedFigure src="open-code-quest-namespaces.png" >}} + +Avant et pendant l'**Open Code Quest**, la gestion de la **plateforme** a joué un rôle clé dans la réussite de l'événement. +En tant que membre organisateur, j’ai eu la responsabilité, avec [Sébastien Lallemand](https://sebastienlallemand.net/), de préparer, dimensionner, installer et configurer les huit clusters OpenShift nécessaires au bon déroulement des ateliers. +Cela comprenait un cluster central, un dédié à l'IA, et six autres réservés aux participants pour leurs missions. +Cette phase cruciale de préparation a permis de garantir une infrastructure stable et performante. +Pendant l’événement, mon rôle de SRE (Site Reliability Engineer) consistait à surveiller de près les métriques critiques, telles que l'utilisation des ressources, afin d'assurer une expérience fluide et optimale pour tous les participants. +Grâce à cette surveillance proactive nous avons pu offir une disponibilité constante des environnements et ainsi faciliter le bon déroulement de l'atelier. + +{{< attachedFigure src="open-code-quest-clusters.png" >}} + +Un autre défi que j'ai relevé pour l'Open Code Quest a été la création d'un **Leaderboard** destiné à favoriser l'émulation entre les participants. +Ce projet m'a demandé de sortir des sentiers battus, car j'ai dû utiliser des outils tels que **Prometheus** et **Grafana** pour une tâche à laquelle ils ne sont pas destinés : départager les participants par ordre d'arrivée. +En contournant les limites de ces technologies de monitoring, j'ai fait preuve de créativité pour concevoir un système de classement en temps réel. +Malgré la complexité technique, le résultat a dépassé nos attentes : le Leaderboard a stimulé la compétition (amicale) entre les participants, ajoutant une dimension dynamique et engageante à l'événement. + +Pour nous, l'**Open Code Quest** a été bien plus qu’un simple atelier. C’était une journée où experts et débutants ont pu échanger, apprendre et s’amuser ensemble, tout en découvrant des technologies utiles aux développeurs et architectes. +Que ce soit pour l’accélération du développement avec Quarkus, la fluidité de l'expérience développeur avec Red Hat Developer Hub, la gestion de la sécurité de la *supply chain* avec **Red Hat Trusted Application Pipelines** ou l'utilisation de l’IA avec Quarkus, chaque outil a apporté une valeur concrète, démontrée au fil des exercices. + +Nous avons également eu l'occasion de créer un environnement propice au réseautage, où les participants ont pu échanger avec des experts et leurs pairs. + +En tant que membre de l'équipe organisatrice, je suis extrêmement fier du succès de l'**Open Code Quest**. +Ce workshop a montré que l’on peut allier apprentissage technique et divertissement dans un cadre immersif et stimulant. Nous remercions tous les participants pour leur engagement et leur enthousiasme, ainsi que nos partenaires pour leur soutien. Nous espérons vous revoir lors de nos prochains événements pour continuer à explorer ensemble les innovations technologiques qui transforment notre monde. + +Envie d'en savoir plus sur le Leaderboard ? +Comment j'ai pris en compte les spécificités de Prometheus pour concevoir le Leaderboard ? +Comment j'ai calibré les bonus et accélérateurs pour favoriser la compétition et l'émulation ? + +Tout est expliqué dans ces deux 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" >}} + +## Démo "Mission Impossible" : Lego, AI & Edge Computing + +Une partie de la journée, j'étais sur le stand RHEL, accompagné de [Adrien](https://www.linkedin.com/in/adrien-legros-78674a133/), [Mourad](https://www.linkedin.com/in/mourad-ouachani-0734218/) et [Pauline](https://www.linkedin.com/in/trg-pauline/) pour installer la démo "Mission Impossible" et répondre aux questions du public. + +Cette démo, nous l'avons conçue pour l'événement {{< internalLink path="/speaking/platform-day-2024/index.md" >}} sur le thème du dernier opus du film **Mission Impossible: Dead Reckoning**. +Dans cette démo, **Ethan Hunt** a besoin d'aide pour arrêter le train **Lego City #60337** avant qu'il ne soit trop tard ! +Rien de moins que le sort de l'humanité est en jeu ! + +{{< attachedFigure src="mission-impossible-plot.png" >}} + +Le scénario nécessite que **Ethan Hunt** monte à bord du train pour y connecter une carte **Nvidia Jetson Orin Nano** au réseau informatique du train et y déploie une IA qui reconnaitra les panneaux de signalisation et arrêtera le train à temps avant qu'il ne déraille ! +Une console permettra d'avoir une vue déportée de la caméra de vidéo surveillance du train, avec les résultats de l'inférence du modèle d'IA incrustés. + +{{< attachedFigure src="mission-impossible-scenario.png" >}} + +Pour mettre en oeuvre cette démo, nous avons équipé le train **Lego** d'une carte **Nvidia Jetson Orin Nano**, d'une webcam et d'une batterie portable. +La carte Nvidia Jetson Orin est un System On Chip (SoC), elle comprend tout le matériel dont **Ethan Hunt** a besoin pour sa mission : CPU, RAM, stockage... +Ainsi qu'un un GPU pour accélérer les calculs ! +Le Jetson reçoit le flux vidéo de la caméra embarquée et transmet les ordres au Hub **Lego** via le protocole **Bluetooth Low Energy**. +Il est alimenté via une batterie portable pour la durée de la mission. + +{{< attachedFigure src="rhel-booth-mission-impossible-demo.jpeg" >}} + +Nous sommes dans un contexte de Edge Computing. +Sur le Jetson, nous avons installé **Red Hat Device Edge**. +C’est une variante de Red Hat Enterprise Linux adaptée aux contraintes du **Edge Computing**. +Nous y avons installé **Microshift**, le Kubernetes de Red Hat taillée pour le Edge. +Et dans Microshift, nous avons déployé *over-the-air* les microservices, un **broker MQTT** et le modèle d’intelligence artificielle. + +Le Jetson est relié, pour la durée de la mission, à un cluster OpenShift dans le cloud AWS via une connexion 5G. +Dans le cloud AWS, il y a une VM RHEL 9 qui nous permet de construire les images **Red Hat Device Edge** pour le SoC Jetson. +Dans le cluster OpenShift, l'application de vidéo surveillance qui diffuse le flux vidéo de la caméra embarquée du train. +Le flux vidéo est relayé depuis le Jetson au travers d’un **broker Kafka** ! +Il faut ajouter à cela des pipelines MLops pour entraîner le modèle d’IA. +Et enfin des pipelines CI/CD pour construire les images de conteneur de nos micro-services pour les architectures x86 et ARM. + +{{< attachedFigure src="mission-impossible-hardware-architecture.png" >}} + +Pour permettre à **Ethan Hunt** de mener à bien sa mission, il a fallu garantir la transmission de la donnée de bout en bout. +Pour cela, nous avons implémenté cinq services qui communiquent via un système d’envoi de messages asynchrone (**MQTT**). + +Le premier service capture dix images par seconde à intervalle régulier. +Chaque image est redimensionnée en 600x400 pixels et encapsulée dans un événement avec un identifiant unique. +Cet événement est transmis au modèle d'IA qui l’enrichit avec le résultat de la prédiction. +Ce dernier est transmis à un service de transformation qui a pour rôle d'extraire l'action du train, la transmettre au contrôleur de train pour ralentir ou stopper le train et en parallèle envoyer l'événement au service de streaming (**Kafka**) déployé sur un Openshift distant, qui affiche en temps réel, les images et la prédiction. + +{{< attachedFigure src="mission-impossible-software-architecture.png" >}} + +Et enfin, il nous a fallu construire un modèle d’intelligence artificielle. +Pour cela, nous avons suivi les bonnes pratiques pour gérer le cycle de vie du modèle, c’est ce qu’on appelle le **MLOps** : + +- **Acquérir la donnée** : Nous avons utilisé un jeu de données open source comprenant des données provenant d’une caméra embarquée sur une voiture, qui ont été annotées avec les panneaux rencontrés sur son trajet. + Les photos ont été prises sur des routes dans l’union européenne et montrent donc des panneaux de signalisation "normalisés" (potentiellement un peu différents des panneaux **Lego**). +- **Développer un modèle d’IA** : Nous avons choisi un algorithme d’apprentissage et procédé à l'entraînement du modèle sur un cluster OpenShift avec des GPU pour accélérer le calcul. +- **Déployer le modèle** : Nous avons déployé le modèle dans un serveur d’inférence pour le consommer via des APIs. + Il a fallu intégrer le modèle à l’architecture logicielle (via MQTT). +- **Mesurer les performances et ré-entraîner** : En observer le comportement du modèle, nous avons pu mesurer la qualité des prédictions et constater que tous les panneaux **Lego** n'était pas bien reconnus. + Nous avons pris la décision de réentrainer le modèle en l’affinant avec un jeu de données enrichi. + +{{< attachedFigure src="mission-impossible-ai.png" >}} + +Si vous n'avez pas pu venir nous voir sur le stand, je vous propose une session de rattrapage dans la vidéo ci-dessous (capturée lors du {{< internalLink path="/speaking/platform-day-2024/index.md" >}}). +On y voit le train s'arrêter lorsqu'il détecte le panneau de signalisation correspondant. + +{{< embeddedVideo src="mission-impossible-demo.mp4" autoplay="true" loop="true" muted="true" width="1920" height="1080" >}} + +Cette démonstration permet de démontrer la pertinence des solutions Red Hat pour mener à bien des projets informatique combinant **Intelligence Artificielle** et **Edge Computing**, et ce à large échelle. + +## Conclusion + +À travers l'atelier **Open Code Quest** et la démonstration captivante du train **Lego**, les participants ont pu explorer des solutions innovantes pour le développement d’applications, l'Intelligence Artificielle, le *Edge Computing* et la sécurité de la *Supply Chain*. +Tout le travail autour de la plateforme ainsi que l'originalité du Leaderboard ont permis de dynamiser l’événement, renforçant la compétition amicale entre les participants tout en leur offrant une expérience technique et humaine que l'on espère inoubliable. + +Pour moi, ce Red Hat Summit Connect a été l'occasion de mettre en valeur l'importance de technologies comme Quarkus et OpenShift, mais aussi de partager une aventure collective où chaque participant a pu repartir avec de nouvelles compétences, de l'inspiration, et l'envie de continuer à explorer ces solutions. +Nous espérons pouvoir continuer à faire évoluer cet événement pour offrir toujours plus de défis et d'innovations aux communautés de développeurs, architectes, et ingénieurs. +À très bientôt pour de nouvelles aventures technologiques ! diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-ai.png b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-ai.png new file mode 100644 index 0000000..5cbf661 Binary files /dev/null and b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-ai.png differ diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-demo.mp4 b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-demo.mp4 new file mode 100644 index 0000000..cc50e44 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-demo.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c64133550cf99f684028144188fcae634992bff29774677bbf679741b2ab9474 +size 4725953 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-hardware-architecture.png b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-hardware-architecture.png new file mode 100644 index 0000000..353bfc5 Binary files /dev/null and b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-hardware-architecture.png differ diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-plot.png b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-plot.png new file mode 100644 index 0000000..3e97263 Binary files /dev/null and b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-plot.png differ diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-scenario.png b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-scenario.png new file mode 100644 index 0000000..297668e Binary files /dev/null and b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-scenario.png differ diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-software-architecture.png b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-software-architecture.png new file mode 100644 index 0000000..9782791 Binary files /dev/null and b/content/french/speaking/red-hat-summit-connect-france-2024/mission-impossible-software-architecture.png differ diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-clusters.png b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-clusters.png new file mode 100644 index 0000000..36f6055 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-clusters.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46060900cf25cb21fbd5b53c7d575747006d9518de81f940f6de06793f3be6b0 +size 419834 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-microservices.png b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-microservices.png new file mode 100644 index 0000000..59845ed --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-microservices.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a7d19d068ea9a42edf8d88d93f1545faedcd1ade403fa3368955780988e97e1 +size 178064 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-namespaces.png b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-namespaces.png new file mode 100644 index 0000000..72547e9 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-namespaces.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea642709f38856491ba4afddb419af698dfbfbc28f7687d1ebc874d384175e2f +size 819783 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage-2.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage-2.jpeg new file mode 100644 index 0000000..0e5e004 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage-2.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:714078951490df5937e8b2280de56dac6222f8c06987dc4fb3889ae53af9c3ec +size 377231 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage.jpeg new file mode 100644 index 0000000..b13c8c6 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-nicolas-masse-on-stage.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:453a12635d96992f94855e85aef003ad5958db64085354c597c81d99e7b6932c +size 156648 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-testimonial.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-testimonial.jpeg new file mode 100644 index 0000000..0f63bf2 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-testimonial.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b72036a60e2b2ce3acdb404ef5aec36cc3dbd9ab8f69e0a9fa1f5b4f7219513 +size 251417 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-winners.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-winners.jpeg new file mode 100644 index 0000000..9bc61d5 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/open-code-quest-winners.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:314a06405cd22bf1192d48c8b3f81361e4a17995fe7b61f974d12b9b9f3172ff +size 998418 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-2.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-2.jpeg new file mode 100644 index 0000000..0c21074 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-2.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:097e47074b2589cd17a3fe0829748a92c3c3bdd230ddc0d731b96aec927fe33f +size 320103 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-3.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-3.jpeg new file mode 100644 index 0000000..3c1c77d --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6b032010a392e3d17ae1c28ecf9a6b0a974866774f1c5e187ab08813873d77e +size 305101 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo.jpeg new file mode 100644 index 0000000..fc3d2b3 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/rhel-booth-mission-impossible-demo.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83b1d4318546838508a1a125d52cb67f94af2aa96f6f3bdbfab3878e86fd0935 +size 374415 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/sources/Platform-Day-2024.mp4 b/content/french/speaking/red-hat-summit-connect-france-2024/sources/Platform-Day-2024.mp4 new file mode 100644 index 0000000..d8d7f50 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/sources/Platform-Day-2024.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f5df3481119bc43e2cd5938165e0c19fe8fc633556f4b182fe376f5c276bbae +size 57976079 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/sources/extract.sh b/content/french/speaking/red-hat-summit-connect-france-2024/sources/extract.sh new file mode 100755 index 0000000..05c8b57 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/sources/extract.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# Source and destination +src="Platform-Day-2024.mp4" +dest="../mission-impossible-demo.mp4" + +# Extract a part of the video +ffmpeg -y -ss 00:00:37.000 -i "$src" -to 00:00:09.500 -an "$dest" diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-1.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-1.jpeg new file mode 100644 index 0000000..ee07421 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-1.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25b7c2d6669307197ec8861b45204fb81cc6277ad234c965aedb4c739c7a3835 +size 400262 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-2.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-2.jpeg new file mode 100644 index 0000000..3af613a --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-2.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeed1fc683e9818155d9eb91cc4a9fc361ef571756aca25fd5b68568379d9faa +size 371122 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-3.jpeg b/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-3.jpeg new file mode 100644 index 0000000..31c7f58 --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/sources/open-code-quest-winners-3.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:169c2d3e7e4ce3c46381a5ce8a0b21e0860557521344e36dad38b6e2726ae689 +size 246024 diff --git a/content/french/speaking/red-hat-summit-connect-france-2024/sources/stack.sh b/content/french/speaking/red-hat-summit-connect-france-2024/sources/stack.sh new file mode 100755 index 0000000..624f02e --- /dev/null +++ b/content/french/speaking/red-hat-summit-connect-france-2024/sources/stack.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -Eeuo pipefail + +magick open-code-quest-winners-{1,2,3}.jpeg +append ../open-code-quest-winners.jpeg + diff --git a/layouts/shortcodes/embeddedVideo.html b/layouts/shortcodes/embeddedVideo.html new file mode 100644 index 0000000..204a883 --- /dev/null +++ b/layouts/shortcodes/embeddedVideo.html @@ -0,0 +1,21 @@ + +{{- $filename := .Get "src" -}} +{{- $video := .Page.Resources.GetMatch (printf "%s" $filename) -}} +{{- $autoplay := default false (.Get "autoplay") -}} +{{- $loop := default false (.Get "loop") -}} +{{- $muted := default false (.Get "muted") -}} +{{- $controls := default false (.Get "controls") -}} +{{- $preload := default "metadata" (.Get "preload") -}} +{{- $height := .Get "height" -}} +{{- $width := .Get "width" -}} + diff --git a/themes/itix b/themes/itix index 18ab2a1..7d4207e 160000 --- a/themes/itix +++ b/themes/itix @@ -1 +1 @@ -Subproject commit 18ab2a17245f97f2ba7b0b477e79ff9afd7e51a3 +Subproject commit 7d4207e94daec3d26b1667d113394fb058c4ad8d