lundi 4 mars 2024

EXP: Logs dynamiques (partie 1) / EXP: Dynamic logs (part 1)

Passons aux choses sérieuses. Dans une suite d'articles, nous allons proposer diverses ressources permettant d'améliorer l'exploitabilité de nos développements. Divers sujets seront abordés: gestion des erreurs, audits, etc. Ce faisant, nous allons petit à petit construire une bibliothèque utilisable sur des projets réels et ce blog lui servira de documentation. Pour les reconnaître facilement, les articles relatifs à l'exploitabilité auront un titre qui commencera par EXP. Dans cet article nous allons parler de logs.

Les logs sont une fonctionnalité de Mulesoft largement utilisée, souvent de façon excessive et anarchique. Les moyens de se renseigner sur le déroulement d'un traitement Mulesoft étant limité (aux logs justement), les développeurs ont tendance à en abuser et très vite, à mélanger les informations générales et particulières. Le système de log devient un véritable fleuve d'informations difficilement utilisables. On se retrouve alors devant un dilemme: être concis pour pouvoir mieux suivre le déroulement d'un processus, ou être bavard afin de se donner une chance de collecter L'information qui nous sera utile en cas d'incident. Heureusement, le système de logs, basé sur Log4J admet la notion de niveau: ERROR, WARN, INFO, DEBUG, TRACE. Le souci est que ce niveau n'est pas ajustable dynamiquement. Pour le modifier, il faut arrêter le serveur et le faire redémarrer. Si l'on est en présence d'un phénomène subit dont on ne comprend pas l'origine, ce redémarrage n'est probablement pas une option:

  1. Redémarrer un serveur en production est un évènement qui généralement réclame une interruption de service, des autorisations, une procédure à suivre et une communication aux utilisateurs: ce n'est pas anodin.
  2. Le simple fait d'arrêter le service et le faire redémarrer peut faire disparaître le phénomène à observer, et il peut se passer un temps assez long avant qu'on ne puisse l'observer à nouveau.
  3. Abaisser le niveau de logs peut engendrer un flots de messages provenant de bien des parties de l'application qui n'ont pas besoin d'un tel niveau de précision dans la surveillance mais qui vont, bien sûr, largement encombrer les fichiers de logs.

Bref, il nous faut quelque chose de plus fin et de plus dynamique. Le mécanisme des niveaux de Log4J est à la fois trop entier et trop rigide. Dans cet article, nous allons proposer une première version des DLog (Dynamic Log) qui vont permettre de changer le niveau d'un log.

Bien sûr, les logs de Mulesoft ne sont pas dynamiques (le niveau ne peut être précisé qu'en "dur"). Il n'est donc pas possible d'utiliser directement le Logger de Mulsoft. Ce que l'on va faire, c'est appeler un subflow, qui en fonction d'une variable d'environnement va diriger le message vers un Logger au niveau souhaité:



Nous allons ignorer un court instant le "Retrieve Log Strategy" qui l'entame. Le "coeur" de notre subflow est un composant Choice qui dirige l'exécution vers un Logger pris parmi sept, c'est à dire un par niveau plus un niveau par défaut (INFO). Chaque branche When définit un prédicat basé sur la valeur d'une variable appelé "log_strategy" qui précise, dans le champ "level", le niveau de log souhaité.


Le composant Logger de la branche est des plus classique. Il se contente de Logger le payload. A charge à l'appelant de fournir dans le payload, la valeur que l'on veut loguer. 


Bon. Jusque là, rien de bien révolutionnaire. Reste à savoir comment nous allons exploiter ce subflow. On imagine que s'il faut:

  1. sauvegarder le payload courant dans une variable,
  2. mettre dans le payload le message à logger,
  3. appeler le logger dynamique,
  4. restaurer le payload sauvegardé

nous allons très considérablement alourdir l'écriture du log. Ce qu'il nous faudrait, c'est un moyen d'effectuer ce log en utilisant un seul composant, comme nous l'aurions fait avec le Logger standard de Mulesoft. Quelque chose qui ressemble à ça:


C'est certes un peu plus "lourd" que le Logger classique, car:

  1. Le composant est un TransformMessage et non un Logger et est donc un peu moins facilement reconnaissable du premier coup d'oeil. C'est pour cela, que par convention, nous feront commencer sa description par un DLOG majuscule.
  2. Le corps du composant enrobe le message à loguer dans l'invocation de la méthode "dlog": il n'est pas fourni seul, ce qui complexifie un peu l'écriture.
Toute la "magie" est bien sûr dans la fonction "dlog" car c'est elle qui doit invoquer le subflow présenté plus haut. Une fonction DataWeave permet d'invoquer un flow: lookup. Noter que lookup peut invoquer un flow, pas un subflow. C'est pourquoi notre subflow possède une partie "source" (c'est donc un flow en réalité). La fonction "dlog" a été isolée dans un module de fonctions personnalisées (Custom function) afin d'alléger l'écriture du log dynamique : elle peut être réutilisée à loisir. Ce fichier est placé dans le répertoire "ressources" du projet:


Le contenu de ce fichier ne contient pour l'instant que la définition de la fonction "dlog". Il a vocation à s'enrichir aux fils des articles de ce blog. La fonction dlog, elle aussi va s'enrichir:

%dw 2.0


fun dlog(log, message)=

Mule::lookup("dlog", log)

then message.payload


Deux choses sont à noter:
  1. la fonction lookup est précédé de la mention de son module: Mule n'est pas importé de facto dans un module de "Custom Function".
  2. dlog retourne le payload du message en cours (afin que le composant DLOG ne change pas le payload et donc n'ait pas d'effet de bord).
Il nous reste à implémenter le moyen de changer dynamiquement le niveau de Log et donc, de modifier le contenu de la variable "log_strategy". Nous allons procéder de façon très classique: utiliser l'Object Store pour faire communiquer le flot qui change cette stratégie avec les flots qui l'utilisent. "Retrieve Log Strategy" de notre subflow de log charge cette stratégie dans la variable "log_strategy".

Pour alimenter l'Object Store nous allons créer un flow HTTP très classique d'import:


Il ne nous reste plus qu'à changer le contenu de notre stratégie pour voir nos messages dynamiques s'afficher, ou non leurs fichiers de logs.

_____________________________________________________________________

Let's get down to business. In a series of articles, we'll be proposing various resources for improving the usability of our developments. We'll be covering a range of topics, from error management to auditing. In doing so, we will gradually build up a library that can be used on real projects, and this blog will serve as its documentation. To make them easily recognizable, articles relating to exploitability will have a title starting with EXP. In this article, we're going to talk about logs.

Logs are a Mulesoft feature that is widely used, often excessively and anarchically. Since the means of finding out about the progress of a Mulesoft process are limited (to logs, in fact), developers tend to overuse them and very quickly mix general and specific information. The log system becomes a veritable river of information that is difficult to use. This leaves us with a dilemma: to be concise in order to better follow the progress of a process, or to be talkative in order to give ourselves a chance to gather information that will be useful in the event of an incident. Fortunately, the Log4J-based logging system supports the notion of levels: ERROR, WARN, INFO, DEBUG, TRACE. The problem is that this level is not dynamically adjustable. To modify it, the server must be stopped and restarted. If you're dealing with a sudden phenomenon whose origin you don't understand, restarting is probably not an option:

  1. Restarting a server in production is an event that generally requires service interruption, authorizations, a procedure to follow and communication to users: it's not trivial.
  2. Simply stopping the service and restarting it may cause the phenomenon to disappear, and it may be some time before it can be observed again.
  3. Lowering the log level can generate a flood of messages from many parts of the application that don't need such a high level of monitoring precision, but which will, of course, clutter up the log files.

    In short, we need something finer and more dynamic. Log4J's level mechanism is both too complete and too rigid. In this article, we're going to propose a first version of DLog (Dynamic Log), which will enable you to change the level of a log.

    Of course, Mulesoft logs are not dynamic (the level can only be specified "hard"). It is therefore not possible to use Mulsoft's Logger directly. What we'll do is call a subflow, which, depending on an environment variable, will direct the message to a Logger at the desired level:



    We'll ignore for a moment the "Retrieve Log Strategy" that starts it off. The "heart" of our subflow is a Choice component which directs execution to one of seven loggers, i.e. one per level plus a default level (INFO). Each When branch defines a predicate based on the value of a variable called "log_strategy" which precises, in the "level" field, the desired log level.


    The Logger component of the branch is a classic. It simply logs the payload. It's up to the caller to supply the payload with the value to be logged.


    So far, so good. So far, nothing revolutionary. The question now is how we're going to exploit this subflow. If we have to:
    1. save the current payload in a variable,
    2. put the message to be logged in the payload,
    3. call the dynamic logger,
    4. restore the saved payload
    and restore the saved payload, we're going to make the log writing considerably more cumbersome. What we need is a way of doing this logging using a single component, as we would have done with the standard Mulesoft Logger. Something like this:


    It's certainly a little "heavier" than the classic Logger, because:
    1. The component is a TransformMessage and not a Logger, and is therefore a little less easily recognizable at first glance. That's why, by convention, we'll start its description with a capital DLOG.
    2. The body of the component wraps the message to be logged in the invocation of the "dlog" method: it is not supplied on its own, which makes writing a little more complex.
    All the "magic" is, of course, in the "dlog" function, since it is this function that must invoke the subflow described above. A DataWeave function can be used to invoke a flow: lookup. Note that lookup can invoke a flow, not a subflow. This is why our subflow has a "source" part (so it's actually a flow). The "dlog" function has been isolated in a custom function module to lighten the load of writing the dynamic log: it can be reused at will. This file is placed in the project's "resources" directory:


    For the moment, this file only contains the definition of the "dlog" function. It will be enriched as this blog is updated. The dlog function will also be enriched:

    %dw 2.0


    fun dlog(log, message)=

    Mule::lookup("dlog", log)

    then message.payload


    Two things are worth noting:
    1. the lookup function is preceded by a mention of its module: Mule is not imported de facto into a "Custom Function" module.
    2. dlog returns the payload of the current message (so that the DLOG component doesn't change the payload and thus have any side effects).
    We now need to implement a way of dynamically changing the Log level, and thus modifying the contents of the "log_strategy" variable. We're going to do this in the classic way: using the Object Store to make the stream that changes this strategy communicate with the streams that use it. Our log subflow's "Retrieve Log Strategy" function loads this strategy into the "log_strategy" variable.

    To feed the Object Store, we'll create a classic HTTP import flow:


    All we need to do now is change the content of our strategy to see our dynamic messages displayed, or not their log files.


    dimanche 3 mars 2024

    Une petite fonction bien utile: then / A useful little function: then

    Nous allons parler aujourd'hui d'une petite fonction assez récente et aussi peu connue qu'utile: then. A ma grande surprise, il n'en est fait que très peu mention aussi bien dans la documentation que dans les formations ou les forums. Evidemment, je n'ai jamais vu quelqu'un d'autre que moi l'utiliser avec des conséquences parfois sévères. Que fait elle ? Et bien rien, en fait. Mais elle le fait merveilleusement bien. Je m'explique. Cette fonction peut se coder (et s'utiliser) de la façon suivante:

    %dw 2.0
    output application/json
    fun then(x, f) = f(x)
    ---
    payload then(x)-> x.message

    Ne la créez pas ainsi si vous utilisez une version 4.3.0 ou supérieure de Mulesoft, car elle est fournie en standard. Si vous êtes obligé d'utiliser une version plus ancienne, dépêchez vous de l'inclure dans votre bibliothèque personnalisée de fonctions utilitaires ! 

    Mais alors à quoi peut elle servir ? Et bien, essentiellement, à deux choses:

    1. éviter de recalculer une valeur
    2. retenir une valeur pour pouvoir "sauter" une ou plusieurs expressions
    Le grand intérêt de then est (uniquement) de créer ce qu'on appelle en programmation, une closure, c'est à dire de rendre visible des ressources (valeur, fonctions) définies au delà de la fonction dans lesquelles elles sont introduites. Je ne m'attarde pas: si vous ne voyez pas de quoi il s'agit, vite interrogez l'ami Google: un peu de lecture vous attend :) . Regardons plutôt en quoi la closure introduite par then peut être utile, dans les deux cas sus cités.

    dans notre premier cas, nous allons exploiter un tableau de nombres. Le but est de filtrer tous les nombres de ce tableau plus grands que la moyenne. Une approche naïve (mais qui est celle de 99.99% des développeurs Mulesoft :( ) est:

    payload filter $ > avg(payload)

    Cette écriture est CATASTROPHIQUE. Pourquoi? Parce que pour CHAQUE nombre dans la liste, la moyenne est recalculée ! Il s'agit donc d'une expression d'ordre quadratique (en NxN). Si votre tableau est petit, vous n'y verrez pas d'inconvénient. Mais si votre tableau est très grand, la performance va s'effondrer ! Avec 10 éléments, vous effectuerez 100 opérations élémentaires, avec 1000, ce sera un million, avec un million, ce sera mille milliards ! La règle à respecter est simple: les expressions quadratiques, c'est JAMAIS. Comment l'éviter ? Très simple:

    avg(payload) then (moy)-> payload filter $>moy

    On calcule la moyenne une fois, on la "stocke" dans le paramètre de la fonction passée à then, et on l'utilise ensuite sans devoir la recalculer.  L'expression devient linéaire (d'ordre N) et devient un million de fois plus efficace si la liste contient un million d'éléments !

    "C'est très bien" diriez-vous, dans un cas aussi simple. Mais comment faire si j'ai besoin de plusieurs valeurs dans mon expression principale (ici filter). Et bien c'est très simple, il suffit d'enchaîner les then. Admettons que nous voulons maintenant récupérer tous les nombres compris entre min+20 et max-20. Nous allons écrire:

    min(payload) then (min)->
    max(payload) then (max)->
    payload filter $>min+20 and $<max-20

    Elégant, non ?

    Autre usage de then: pouvoir enchainer et "sauter" une ou plusieurs expressions. Pour cela laissez moi introduire une autre fonction de DataWeave injustement méconnue: log (oui, oui, elle existe, vous ne rêvez pas!). Notre propos, ici est de logger le commencement de notre traitement, puis de l'exécuter, puis enfin de logger sa conclusion avec pour chaque ligne de log un timestamp afin de pouvoir vérifier l'efficacité de notre petit traitement. Voici la nouvelle version:

    log("Start:" ++ now()) then (l1)-> (
    min(payload) then (min)->
    max(payload) then (max)->
    payload filter $>min+20 and $<max-20) then (result)->
    log("End:" ++ now())

    Voila. Nous avons utilisé then dans son usage le plus basique: enchaîner des expressions. Et notre log contient bien les deux lignes souhaitées:

    "Start:2024-03-03T12:56:04.573778Z"
    "End:2024-03-03T12:56:04.574056Z"

    Mais que ce passe-t-il ? Notre expression vaut maintenant "End:2024-03-03T12:56:04.574056Z"  et non notre liste filtrée ! En effet, le dernier élément de notre suite d'expressions, c'est la dernière, le log final qui l'emporte ! Et ce log "vaut" la chaîne de caractères qu'il logue. Pas de panique. Il suffit de compléter notre petit traitement, pour qu'il retourne la valeur souhaitée, valeur prise dans la closure de notre filtrage (result):

    log("Start:" ++ now()) then (l1)-> (
    min(payload) then (min)->
    max(payload) then (max)->
    payload filter $>min+20 and $<max-20) then (result)->
    log("End:" ++ now()) then (l2)->
    result

    Noter que les closures des deux logs sont ignorées. Elle ne sont nommées que parce que la syntaxe de DataWeave l'impose. Dans ce second cas, on voit bien l'utilisation de la closure qui permet de bypasser le log et de "revenir" à la valeur qui nous intéresse.
    _________________________________________________________________________

    Today, we're going to talk about a fairly recent feature that's as little known as it is useful: then. To my great surprise, very little mention is made of it in documentation, training courses or forums. Obviously, I've never seen anyone but myself use it, sometimes with severe consequences. What does it do? Well, nothing, really. But it does it wonderfully well. Let me explain. This function can be coded (and used) as follows:

    %dw 2.0
    output application/json
    fun then(x, f) = f(x)
    ---
    payload then(x)-> x.message

    Don't create it this way if you're using Mulesoft version 4.3.0 or higher, as it comes as standard. If you have to use an older version, hurry up and include it in your custom library of utility functions! 

    So what can it be used for? Well, essentially, two things:

    1. to avoid recalculating a value
    2. to retain a value in order to skip one or more expressions.

    The main purpose of then is (solely) to create what is known in programming as a closure, i.e. to make visible resources (values, functions) defined beyond the function into which they are introduced. I won't dwell on it: if you don't see what it's all about, just ask your Google friend: a bit of reading awaits you :) . Instead, let's look at how the closure introduced by then can be useful, in the two cases mentioned above.

    In our first case, we're going to use an array of numbers. The aim is to filter out all the numbers in this array that are greater than the mean. A naive approach (but one that 99.99% of Mulesoft developers take :( ) is:

    payload filter $ > avg(payload)

    This writing is CATASTROPHIC. Why is it so? Because for EACH number in the list, the average is recalculated! It's a quadratic expression (in NxN). If your table is small, you won't mind. But if your array is very large, performance will plummet! With 10 elements, you'll perform 100 elementary operations, with 1000, it'll be a million, with a million, it'll be a thousand billion! The rule is simple: quadratic expressions are NEVER used. How to avoid them? Very simply:

    avg(payload) then (moy)-> payload filter $>moy

    We calculate the average once, "store" it in the parameter of the function passed to then, and then use it without having to recalculate it.  The expression becomes linear (of order N) and becomes a million times more efficient if the list contains a million elements!

    "That's all very well" you might say, in such a simple case. But what if I need several values in my main expression (here filter)? Well, it's very simple: just chain the then. Let's say we now want to retrieve all the numbers between min+20 and max-20. We'll write:

    min(payload) then (min)->
    max(payload) then (max)->
    payload filter $>min+20 and $<max-20

    Elegant, isn't it?

    Another use for then is to be able to "skip" one or more expressions. To do this, let me introduce another DataWeave function that's unfairly overlooked: log (yes, it exists, you don't dream!). Our purpose here is to log the start of our processing, then to execute it, and finally to log its conclusion with a timestamp for each log line, so that we can check the efficiency of our little processing. Here's the new version:

    log("Start:" ++ now()) then (l1)-> (
    min(payload) then (min)->
    max(payload) then (max)->
    payload filter $>min+20 and $<max-20) then (result)->
    log("End:" ++ now())

    That's it. We've used then in its most basic form: chaining expressions. And our log contains the two lines we wanted:

    "Start:2024-03-03T12:56:04.573778Z"
    "End:2024-03-03T12:56:04.574056Z"

    But what's going on? Our expression is now worth "End:2024-03-03T12:56:04.574056Z" and not our filtered list! In fact, the last element of our expression sequence is the final log! And this log is "worth" the string it logs. No need to panic. All we need to do is complete our little process, so that it returns the desired value, taken from the closure of our filtering (result):

    log("Start:" ++ now()) then (l1)-> (
    min(payload) then (min)->
    max(payload) then (max)->
    payload filter $>min+20 and $<max-20) then (result)->
    log("End:" ++ now()) then (l2)->
    result

    Note that the closures of the two logs are ignored. They are only named because DataWeave syntax requires it. In this second case, you can clearly see the use of the closure, which allows you to bypass the log and "return" to the value of interest.




    mardi 20 février 2024

    Création d'un document JSON par étape / Step-by-step creation of a JSON document

    Générer un fichier de données est à priori une tâche aisée avec Mulesoft. Sur fichier et par SFTP, divers composants permettent d'écrire (Write - mode OVERWRITE) ou de compléter (Write - mode APPEND) le contenu d'un fichier. Un besoin récurrent est de créer un fichier au format JSON car l'usage de CSV - plus simple - est parfois insuffisant car très fragile:

    1. Le choix d'un séparateur est parfois un dilemme cornélien (quel est le caractère imprimable dont on n'est sûr qu'il ne sera JAMAIS utilisé, dans un environnement de plus en plus international)
    2. Comment intégrer dans un fichier des données de type texte (contenant des retours à la ligne) que CSV interprète faussement comme étant plusieurs lignes de données distinctes
    3. CSV est mal adapté pour contenir des données structurées (des objets contenant une ou plusieurs sous listes).

    Le problème est que s'il est très facile de créer un fichier contenant une liste au format JSON en une seule opération, le faire en plusieurs appels n'est pas évident. En effet, écrire plusieurs sous listes au format JSON ne constitue pas un document bien formé car JSON impose qu'un document ne contienne qu'un seul objet racine (objet ou tableau). Or constituer une unique liste a écrire d'un seul coup n'est pas toujours possible:

    1. soit parce que la liste est trop longue (des centaines de milliers ou des millions de lignes)
    2. soit parce que les données à collecter le sont dans des processus successifs et non dans un unique processus (par lectures successives d'une source de données par exemple)

    Construire une liste JSON par enrichissements successifs est en fait assez simple à réaliser. Comme bien des développeurs se sentent démunis face à ce problème, je fournis ici la manière de faire. Nous allons construire trois sous-flots:

    1. un flot d'ouverture qui entame la liste. Ce flot doit être appelé en premier lieu, une unique fois (obligatoire),
    2. un flot d'enrichissement qui peut être appelé autant de fois que nécessaire (éventuellement pas du tout),
    3. un flot de clôture qui ferme la liste. Ce flot doit être appelé en dernier lieu, une unique fois (obligatoire).

    Le flot d'ouverture doit ressembler à celui-ci:


    Le code Dataweave qui assure le formatage du contenu à écrire est le suivant:

    %dw 2.0
    output text/plain
    ---
    "[" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

    Bien que ne tenant que sur quatre lignes, cette snippet mérite quelques explications:

    1. Le format de sortie est "text/plain" afin que Mulesoft ne nous entrave pas pour des questions de format. En effet, l'objet que nous allons écrire, sera incomplet et donc rejeté par JSON.
    2. La structure du tableau (c'est à dire le "[" ouvrant et le "," qui sépare les objets de la liste sont ajoutés par la snippet elle-même. La description des objets est elle déléguée à la fonction DataWeave "Write".
    3. L'écriture se fait en mode OVERWRITE pour s'assurer que le contenu du fichier est correctement réinitialisé.
    4. Noter l'absence du "]" fermant, permettant de venir enrichir le document après ce premier appel. Noter aussi que pour le document doit bien formé, il ne manque que ce "]".

    Le flot d'enrichissement doit ressembler à celui-ci:


    Le code Dataweave qui assure le formatage de l'enrichissement est le suivant:

    %dw 2.0
    output text/plain
    ---
    ",\n" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

     Cette snippet ressemble beaucoup à la précédente. Pointons les quelques différences:

    1. La continuité de la structure du tableau, c'est à dire le "," qui permet de rajouter des produits à la liste déjà présente et le "," qui sépare les objets de la suite de la liste sont ajoutés par la snippet elle-même.
    2. L'écriture se fait en mode APPEND pour que le contenu déjà présent ne soit pas écrasé.
    3. Noter l'absence des "[" ouvrant et "]" fermant, permettant de s'inscrire dans l'enrichissement du document.

    Le flot de clôture est beaucoup plus simple. On se contente, en me APPEND, d'écrire le "]" fermant.

    Ce système fonctionne quelque soit la taille de la liste (y comprit une liste vide) à deux conditions:

    1. qu'un segment de liste ne soit pas trop grand (et "tienne" dans une chaine de caractères DataWeave),
    2. que le flot d'ouverture ne présente une liste vide QUE si la liste globale est réellement vide, sinon, elle doit contenir au moins un objet.
    3. que le flot d'enrichissement ne présente JAMAIS une liste vide (elle doit contenir au moins un objet).

    ________________________________________________________________________

    Generating a data file is, on the face of it, an easy task with Mulesoft. On file and via SFTP, various components enable you to write (Write - OVERWRITE mode) or complete (Write - APPEND mode) the contents of a file. A recurring need is to create a file in JSON format, as the simpler CSV format is sometimes insufficient due to its fragility:

    1. Choosing a separator is sometimes a Cornelian dilemma (which printable character can you be sure will NEVER be used, in an increasingly international environment).
    2. How to integrate text data (containing line breaks) into a file, which CSV misinterprets as several separate lines of data?
    3. CSV is ill-suited to holding structured data (objects containing one or more sub-lists).

    The problem is that while it's very easy to create a file containing a list in JSON format in a single operation, doing so in several calls is not straightforward. Indeed, writing several sub-lists in JSON format does not constitute a well-formed document, as JSON requires that a document contain only one root object (object or array). However, it's not always possible to write a single list all at once:

    1. either because the list is too long (hundreds of thousands or millions of lines)
    2. or because the data to be collected are collected in successive processes rather than in a single process (by successive readings of a data source, for example).

    Building a JSON list by successive enrichments is actually quite simple to achieve. As many developers find themselves at a loss when confronted with this problem, I've provided a step-by-step guide here. We're going to build three sub-flows:

    1. an opening stream that starts the list. This stream must be called first, once only (mandatory),
    2. an enrichment stream, which can be called as many times as necessary (or not at all),
    3. a closing flow, which closes the list. This stream must be called last, once only (mandatory).

    The opening flow should look like this:


    The Dataweave code used to format the content to be written is as follows:

    %dw 2.0
    output text/plain
    ---
    "[" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

    Although only four lines long, this snippet deserves some explanation:

    1. The output format is "text/plain", so that Mulesoft doesn't hinder us with format questions. Indeed, the object we're about to write will be incomplete and therefore rejected by JSON.
    2. The structure of the array (i.e. the opening "[" and the "," separating the objects in the list) is added by the snippet itself. Object description is delegated to the DataWeave "Write" function.
    3. Writing is done in OVERWRITE mode to ensure that file contents are correctly reset.
    4. Note the absence of the closing "]", allowing the document to be enriched after this first call. Note also that, for the well-formed document, only the "]" is missing.

    The enrichment flow should look like this:


    The Dataweave code used for enrichment formatting is as follows:

    %dw 2.0
    output text/plain
    ---
    ",\n" ++ (payload map write($, "application/json") reduce ($$ ++ ",\n" ++ $))

    This snippet is very similar to the previous one. Let's point out the few differences:

    1. The continuity of the table structure, i.e. the "," that allows products to be added to the list already present and the "," that separates objects from the rest of the list are added by the snippet itself.
    2. Writing is done in APPEND mode, so that existing content is not overwritten.
    3. Note the absence of opening and closing "[" and "]", allowing you to take part in the enrichment of the document.

    The closing flow is much simpler. In APPEND mode, you simply write the closing "]".

    This system works regardless of the size of the list (including an empty list) on two conditions:

    1. a list segment is not too large (and "fits" into a DataWeave string),
    2. the opening stream presents an empty list ONLY if the global list is truly empty, otherwise it must contain at least one object.
    3. the enrichment flow NEVER presents an empty list (it must contain at least one object).


    Regroupement et Structuration / Grouping and structuring

    Voici un article qui devrait vous être très utile: comment transformer un CSV en une liste d'objets structurés. On me dira: mais c'est très simple avec Mulesoft, il suffit de passer de application/csv à application/json et le tour et joué. C'est le cas, en effet, pour des objets strictement tabluaires, c'est à dire que chaque ligne représente un objet qui ne contient que des champs à valeur litérale. Mais quand est-il si l'objet est structuré, c'est à dire si N lignes de CSV représente UN objet contenant un champ évalué avec une sous liste de N objets ? Là, il faut effectuer une transformation. C'est le principe de celle-ci dont il est question dans cet article.

    Entrons dans le concret. Soit l'exemple sylvestre suivant, qui décrit des arbres dans une foret. Les colonnes communes sont forest, tree, trunk et root. Les informations à mettre dans les objets secondaires (les branches des arbres) sont branch et leave. Enfin, les informations qui permettent d'identifier les arbres sont forest et tree (et qui sont aussi des données communes). Donc à l'entrée, on a ça:

    forest,tree,trunk,root,branch,leaves
    f,A,12cm,3m,1,110
    f,A,12cm,3m,2,220
    F,A,18cm,8m,1,150
    F,A,18cm,8m,2,270
    f,B,110cm,12m,1,310
    f,B,110cm,12m,2,320
    f,B,110cm,12m,3,340
    f,C,16cm,4m,1,230

    Et en sortie, on veut obtenir cela:

    [
    {
    "forest": "f",
    "tree": "A",
    "trunk": "12cm",
    "root": "3m",
    "branches": [
    {
    "branch": "1",
    "leaves": "110"
    },
    {
    "branch": "2",
    "leaves": "220"
    }
    ]
    },
    {
    "forest": "F",
    "tree": "A",
    "trunk": "18cm",
    "root": "8m",
    "branches": [
    {
    "branch": "1",
    "leaves": "150"
    },
    {
    "branch": "2",
    "leaves": "270"
    }
    ]
    },
    {
    "forest": "f",
    "tree": "B",
    "trunk": "110cm",
    "root": "12m",
    "branches": [
    {
    "branch": "1",
    "leaves": "310"
    },
    {
    "branch": "2",
    "leaves": "320"
    },
    {
    "branch": "3",
    "leaves": "340"
    }
    ]
    },
    {
    "forest": "f",
    "tree": "C",
    "trunk": "16cm",
    "root": "4m",
    "branches": [
    {
    "branch": "1",
    "leaves": "230"
    }
    ]
    }
    ]

    La fonction qui permet de construire les objets structurés est la suivante. Elle a été écrite afin d'être générique dans le cas d'objets ne contenant qu'une sous liste d'objets simples (donc non structurés à leur tour) afin de pouvoir être utilisée tel quel. C'est le cas le plus courant et elle devrait donc être suffisante dans la majorité des cas. Il faudra l'adapter pour qu'elle puisse générer des objets contenant plusieurs sous listes, ou des objets qui ont des listes d'objets possédant eux même leur propre sous liste.

    Cette fonction prend quatre paramètres:

    1. lst: la liste des objets tabulaires bruts provenant de CSV
    2. id: un tableau contenant les noms des champs identifiants les objets
    3. common: un tableau contenant les noms des champs commun (c'est à dire à placer dans les objets racines). Les champs qui ne sont pas dans cette liste commune sont automatiquement affectés aux objets inclus.
    4. sublist: le nom du champ de l'objet principal qui désigne la liste des objets inclus.

    %dw 2.0
    output application/json
    fun group(lst, id, common, sublist) = lst
    groupBy ((ln) -> (id reduce (fld, acc="")-> acc ++ ln[fld] ++ ","))
    mapObject ((o, i) -> (i):(
    (common reduce (v, acc={})->acc ++ (v):o[0][v]))
    ++ (sublist): o map (b) -> b -- common
    )
    pluck $
    ---
    group(payload, ["forest", "tree"],
        ["forest", "tree", "trunk", "root"], "branches")

    Le principe de cette fonction est le suivant:

    1. Les lignes CSV sont d'abord regroupées en fonction d'une "clé" qui est la concaténation des valeurs des champs d'identification (par groupBy)
    2. Ensuite chaque sous liste obtenue dans l'étape précédente est passée à une réduction qui copie les champs "communs" de la première ligne dans un objet
    3. Les champs communs de chaque ligne de la sous liste sont retranchés (b -- common) et la sous liste est ajoutée sous la forme d'un champ de l'objet créé lors de l'étape précedevte (++ (sublist):)  
    4. Par un appel à pluck, on transforme la Map obtenue par GroupBy en une liste.
    ________________________________________________________________________

    Here's an article that should come in very handy: how to transform a CSV into a list of structured objects. Some people will say: but it's very simple with Mulesoft, you just have to switch from application/csv to application/json and you're done. This is indeed the case for strictly tabular objects, i.e. where each line represents an object containing only literal fields. But what if the object is structured, i.e. if N CSV lines represent ONE object containing a field evaluated with a sub-list of N objects? In this case, a transformation is required. That's what this article is all about.

    Let's get down to the nitty-gritty. Consider the following sylvan example, which describes trees in a forest. The common columns are forest, tree, trunk and root. Secondary objects (tree branches) are branch and leave. Finally, the information used to identify the trees is forest and tree (which are also common data). So this is the input:

    forest,tree,trunk,root,branch,leaves
    f,A,12cm,3m,1,110
    f,A,12cm,3m,2,220
    F,A,18cm,8m,1,150
    F,A,18cm,8m,2,270
    f,B,110cm,12m,1,310
    f,B,110cm,12m,2,320
    f,B,110cm,12m,3,340
    f,C,16cm,4m,1,230

    And that's what we want to achieve:

    [
    {
    "forest": "f",
    "tree": "A",
    "trunk": "12cm",
    "root": "3m",
    "branches": [
    {
    "branch": "1",
    "leaves": "110"
    },
    {
    "branch": "2",
    "leaves": "220"
    }
    ]
    },
    {
    "forest": "F",
    "tree": "A",
    "trunk": "18cm",
    "root": "8m",
    "branches": [
    {
    "branch": "1",
    "leaves": "150"
    },
    {
    "branch": "2",
    "leaves": "270"
    }
    ]
    },
    {
    "forest": "f",
    "tree": "B",
    "trunk": "110cm",
    "root": "12m",
    "branches": [
    {
    "branch": "1",
    "leaves": "310"
    },
    {
    "branch": "2",
    "leaves": "320"
    },
    {
    "branch": "3",
    "leaves": "340"
    }
    ]
    },
    {
    "forest": "f",
    "tree": "C",
    "trunk": "16cm",
    "root": "4m",
    "branches": [
    {
    "branch": "1",
    "leaves": "230"
    }
    ]
    }
    ]

    The function used to construct structured objects is as follows. It has been written to be generic in the case of objects containing only a sub-list of simple objects (i.e. unstructured in turn), so that it can be used as is. This is the most common case, and should therefore be sufficient in the majority of cases. It will need to be adapted so that it can generate objects containing several sub-lists, or objects that have lists of objects themselves possessing their own sub-lists.

    This function takes four parameters:

    1. lst: the list of raw tabular objects from CSV
    2. id: an array containing the names of the fields identifying the objects
    3. common: an array containing the names of common fields (i.e. to be placed in the root objects). Fields not in this common list are automatically assigned to the included objects.
    4. sublist: the name of the main object field that designates the list of included objects.

    %dw 2.0
    output application/json
    fun group(lst, id, common, sublist) = lst
    groupBy ((ln) -> (id reduce (fld, acc="")-> acc ++ ln[fld] ++ ","))
    mapObject ((o, i) -> (i):(
    (common reduce (v, acc={})->acc ++ (v):o[0][v]))
    ++ (sublist): o map (b) -> b -- common
    )
    pluck $
    ---
    group(payload, ["forest", "tree"],
        ["forest", "tree", "trunk", "root"], "branches")

    This function works as follows:

    1. CSV rows are first grouped according to a "key" which is the concatenation of the values of the identifying fields (by groupBy).
    2. Next, each sub-list obtained in the previous step is passed to a reduction that copies the "common" fields of the first line into an object.
    3. The common fields of each line of the sublist are subtracted (b -- common) and the sublist is added as a field of the object created in the previous step (++ (sublist):)  
    4. A call to pluck transforms the Map obtained by GroupBy into a list.

    lundi 19 février 2024

    Un essai de formalisation de la qualité / An attempt to formalize quality

    La maîtrise de la plateforme Mulesoft en général et de Dataweave en particulier étant en soi un sujet difficile, bien des développeurs s'arrêtent à un unique objectif: arriver au résultat demandé en passant par pertes et profit tout le reste. Une telle attitude est catastrophique car dès qu'on tente d'utiliser ce qui est fait en condition réelle, c'est le carnage: incidents à répétition, temps de réponse très au delà du prohibitif, impossibilité de comprendre ce qui se passe, etc, etc.

    Dans toute réalisation informatique la question de la qualité est INCONTOURNABLE et le seul fait que "ça marche" est à des kilomètres d'être suffisant. Pour prendre une analogie, c'est toute la différence entre les mots "comestible" et "cuisine". Mais comme la notion de "qualité" en informatique est bien trop vague pour n'être autre chose qu'une invocation et un vœu pieux, permettez moi d'en apporter ici une définition un peu plus structurée. Selon mon analyse, la qualité se divise en sept critères, donnés ici par ordre décroissant d'importance:

    1. La Robustesse
    2. La Performance
    3. La Justesse
    4. La Résilience
    5. L'Observabilité
    6. L'Evolutivité
    7. La Lisibilité

    Avant d'entrer dans le détail de chacun de ses critères, arrêtons nous un instant sur cette notion de priorité qui peut légitimement interroger. Les deux premiers critères sont ESSENTIELS, c'est à dire que s'ils sont mal remplis:

    1. L'application n'est pas utilisable et
    2. Il y a une forte probabilité pour que l'application ne soit pas amendable (et donc qu'il faut tout jeter !)

    Les deux critères suivants sont CRUCIAUX, c'est à dire que s'ils ne sont pas remplis:

    1. L'application n'est pas utilisable mais
    2. L'application est amendable (et donc on peut corriger les problèmes, ouf !)

    Les deux derniers critères sont SERIEUX, c'est à dire que s'il ne sont pas remplis:

    1. L'application est néanmoins utilisable mais
    2. Sa pérennité est menacée à terme

    Donc lorsqu'il y a des arbitrages à faire, il faut bien respecter ces priorités. Les critères cruciaux menacent tout, y compris le travail accomplis: ils ne peuvent ne aucun cas être négligés et doivent être résolus dès qu'ils se présentent. Les deux suivants sont indispensable, mais ne provoque généralement qu'une perte de temps. Les deux derniers sont un "(Very) Nice to Have". Passons maintenant ces critères en revue:

    La robustesse: c'est la capacité de l'application à s'exécuter sans incident, c'est à dire ne pas planter, ne pas freezer, ne pas entrer dans des verrous mortels. Tout le monde comprend que cet aspect est indispensable. Ce qui est moins courant, c'est la conscience que parfois, la robustesse est tellement mal gérée, qu'il est nécessaire de tout refaire pour l'assurer, d'où le caractère absolument critique de ce critère.

    La performance: c'est la capacité de l'application à être économe des ressources à sa disposition, qu'il s'agisse du temps d'exécution (la définition la plus commune de la performance), mais aussi la mémoire utilisée, la bande passante réseau, l'espace disque, etc. C'est un critère qui est souvent négligé par les développeurs comme étant accessoire ou amendable. C'est souvent le cas, mais pas toujours et parfois, retrouver de bonnes performances nécessite de revoir toute la conception, d'où le danger inhérent de ce critère.

    La justesse: c'est la capacité de l'application de répondre correctement, c'est à dire de restituer les informations ou les commandes attendues par l'utilisateur. C'est la qualité "business" de l'application. C'est très souvent LE critère privilégié des développeurs, et même très souvent le SEUL assuré. Sans en négliger son importance, il ne faut pas oublier que les problèmes de ce type sont - sauf cas très exceptionnels - corrigeables.

    La résilience: c'est la capacité de l'application de répondre aux sollicitations agressives, erronées ou défaillantes. C'est la robustesse "externe" en quelque sorte. Un des sujets majeurs de la résilience est la sécurité (sous toutes ses formes). L'autre gros sujet, c'est la disponibilité des autres pièces du SI qui doivent communiquer avec l'application. La résilience est très souvent un sujet qui négligé et le corriger consiste en fait à écrire le code nécessaire, car il est manquant

    L'Observabilité: c'est la capacité de l'application à fournir des informations sur son fonctionnement. On y inclus le système de log, l'usage possible d'un débogueurs, la génération de rapport d'exécution, etc. De cette aspect, dépend la faculté des développeurs à identifier la raison d'un problème.

    L'évolutivité: c'est la capacité de l'application à être étendue afin d'intégrer de nouvelles fonctionnalités sans remise en cause fondamentale.

    La lisibilité: c'est la capacité de l'application à être comprise par un développeur qui n'a pas - ou peu - participer à son écriture.

    Il arrive parfois que certains critères soient en contradiction avec d'autres. C'est en particulier le cas entre celui des performances (qui peut entrainer un accroissement de la complexité) et ceux de l'évolutivité et de la lisibilité (qui supportent assez mal l'accroissement de la complexité). Pour faire les choix pertinents, il est nécessaire de se référer aux priorités telles que données ci-dessus et la gravité des problèmes rencontrés (par exemple, s'il est licite d'accroitre la complexité pour améliorer significativement des performances réellement insuffisantes, il ne l'est pas forcément pour faire des optimisations pour une réalisation déjà suffisamment performante).

    ______________________________________________________________

    Because mastering the Mulesoft platform in general, and Dataweave in particular, is in itself a difficult subject, many developers focus on a single objective: to achieve the required result, and write off everything else. Such an attitude is catastrophic, because as soon as you try to use what's been done in real-life conditions, it's carnage: repeated incidents, response times that are far beyond prohibitive, the impossibility of understanding what's going on, etc., etc.

    In all IT projects, the question of quality is INCREDIBLE, and the mere fact that "it works" is miles from being sufficient. To take an analogy, this is the difference between the words "edible" and "cooking". But as the notion of "quality" in computing is far too vague to be anything more than an invocation and wishful thinking, allow me to provide here a slightly more structured definition. According to my analysis, quality can be divided into seven criterion, listed here in descending order of importance:

    1. Robustness
    2. Performance
    3. Correctness
    4. Resilience
    5. Observability
    6. Scalability
    7. Legibility

    Before going into the details of each of these criteria, let's stop for a moment to consider the notion of priority, which may legitimately raise questions. The first two criteria are ESSENTIAL, i.e. if they are not met, the application cannot be used:

    1. The application is not usable and
    2. There's a high probability that the application won't be amendable (and therefore has to be thrown out!).

    The next two criteria are CRUCIAL, i.e. if they are not met:

    1. The application is not usable, but
    2. The application is amendable (and so the problems can be corrected, phew!)

    The last two criteria are SERIOUS, i.e. if they are not met:

    1. The application is still usable, but
    2. Its long-term viability is threatened
    Robustness: this is the ability of the application to run without incident, i.e. not to crash, freeze or enter deadlocks. Everyone understands that this aspect is essential. What is less common is the awareness that sometimes robustness is so poorly managed that it's necessary to redo everything to ensure it, hence the absolutely critical nature of this criterion.

    Performance: this is the ability of the application to be economical with the resources at its disposal, whether in terms of execution time (the most common definition of performance), but also memory used, network bandwidth, disk space, etc. It's a criterion that is often overlooked in the design of applications. This is a criterion that is often overlooked by developers as being incidental or amendable. This is often the case, but not always, and sometimes restoring good performance requires a complete rethink of the design, hence the inherent danger of this criterion.

    Correctness: this is the ability of the application to respond correctly, i.e. to deliver the information or commands expected by the user. This is the application's "business" quality. It is very often THE preferred criterion for developers, and very often the ONLY one guaranteed. While not neglecting its importance, it should not be forgotten that problems of this type are - except in very exceptional cases - correctable.

    Resilience: the application's ability to respond to aggressive, erroneous or faulty requests. It's a kind of "external" robustness. One of the major subjects of resilience is security (in all its forms). The other major issue is the availability of other parts of the IS that need to communicate with the application. Resilience is very often a neglected subject, and correcting it means writing the necessary code, because it's missing.

    Observability: this is the ability of the application to provide information about its operation. This includes the logging system, the possible use of debuggers, the generation of execution reports, etc. The ability of developers to identify the reason for a problem depends on this aspect.

    Scalability: this is the ability of the application to be extended to integrate new functionalities without fundamental changes.

    Readability: this is the ability of the application to be understood by a developer who has had little or no involvement in writing it.

    Sometimes, certain criteria are in contradiction with others. This is particularly the case between performance (which can lead to an increase in complexity) and scalability and readability (which don't cope well with an increase in complexity). To make the right choices, it is necessary to refer to the priorities given above and the seriousness of the problems encountered (for example, while it is legitimate to increase complexity in order to significantly improve performance that is really insufficient, it is not necessarily the case to make optimizations for a project that is already sufficiently efficient).

    vendredi 9 février 2024

    Logger les temps d'exécution / Time Execution Logging

    Qui n'a pas rencontré de problème de performance en écrivant des traitements DataWeave un peu complexe? Pour résoudre de tels problèmes, il est nécessaire  de pouvoir faire afficher le temps d'exécution de la procédure problématique et ensuite suivre ce temps d'exécution. Bien des développeurs ignorent que DataWeave possède tout ce qui est nécessaire  pour implémenter une telle fonctionnalité:

    1. la méthode now() permet de récupérer une estampille temporelle
    2. la méthode log() permet d'émettre un message de log
    3. il est possible de passer une fonction comme paramètre d'une autre fonction

    Nous allons voir tout ça. Prenons par exemple un traitement qui s'exécute dans un temps non négligeable (elle construit un objet de 10000 champs, mais l'intérêt de l'article n'est pas là).

    %dw 2.0
    output application/json
    ---
    (0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)

    Pour monitorer le temps d'exécution de ce traitement, nous allons définir puis utiliser une fonction "eval" que vous pourrez utiliser telle quelle dans vos projet:

    %dw 2.0
    output application/json
    fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
    fun eval(prs) =
    Now() then (t1)-> prs() then (r)->
    log(Now() -t1)
    then r
    ---
    eval(()->
    (0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)
    )

    Quelques commentaires sur cette snippet:

    •  D'abord, il a été nécessaire de redéfinir une fonction Now au dessus de la fonction standard now() car "now() as a Number" retourne un nombre de secondes et pas de millisecondes (ce que fait now()).
    • La fonction "eval" reçoit une fonction en paramètre qu'elle exécute au milieu d'une séquence de trois appels. Les deux appels qui l'encadrent invoquent Now() et en retirent un délai qui est loggé. Noter que "eval" renvoie la valeur obtenue par évaluation de la fonction passée en paramètre: son comportement est donc complètement générique et transparent.
    • Noter le "()->" qui suit l'invocation d'éval. Il sert à transformer l'expression que l'on veux monitorer en une fonction.
    • "eval" est utilisé ici pour monitorer toute l'expression. Rien n'empêche de l'utiliser pour monitorer un morceau de l'expression. On peut écrire par exemple:
    %dw 2.0
    output application/json
    fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
    fun eval(prs) =
    Now() then (t1)-> prs() then (r)->
    log(Now() -t1)
    then r
    ---
    (0 to 10000) reduce(v, acc={})->
        (eval(()->(acc ++ ("value"++(v as Number)):v)))

    __________________________________________________________________________

    Who hasn't encountered performance problems when writing complex DataWeave processes? To solve such problems, it is necessary to be able to display the execution time of the problematic procedure and then track this execution time. Many developers are unaware that DataWeave has everything needed to implement such a feature:

    1. the now() method can be used to retrieve a time stamp
    2. the log() method can be used to send a log message
    3. a function can be passed as a parameter to another function.

    Let's take a look at all this. Let's take, for example, a process that takes a considerable amount of time to run (it builds an object with 10,000 fields, but that's not the point of this article).

    %dw 2.0
    output application/json
    ---
    (0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)

    To monitor the execution time of this process, we're going to define and use an "eval" function that you can use as is in your projects:

    %dw 2.0
    output application/json
    fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
    fun eval(prs) =
    Now() then (t1)-> prs() then (r)->
    log(Now() -t1)
    then r
    ---
    eval(()->
    (0 to 10000) reduce(v, acc={})->(acc ++ ("value"++(v as Number)):v)
    )

    A few comments on this snippet:

    •  Firstly, it was necessary to redefine a Now function above the standard now() function, as "now() as a Number" returns a number of seconds and not milliseconds (which is what now() does).
    • The "eval" function receives a function as a parameter, which it executes in the middle of a sequence of three calls. The two calls that flank it invoke Now() and retrieve a delay that is logged. Note that "eval" returns the value obtained by evaluating the function passed in parameter: its behavior is therefore completely generic and transparent.
    • Note the "()->" that follows the invocation of "eval". It is used to transform the expression you wish to monitor into a function.
    • "eval" is used here to monitor the whole expression. Nothing prevents you from using it to monitor a part of the expression. For example, you could write:
    %dw 2.0
    output application/json
    fun Now() = now() then (t)->(t as Number)*1000 + t.milliseconds
    fun eval(prs) =
    Now() then (t1)-> prs() then (r)->
    log(Now() -t1)
    then r
    ---
    (0 to 10000) reduce(v, acc={})->
        (eval(()->(acc ++ ("value"++(v as Number)):v)))




    Pourquoi ce blog ? / Why this blog?

    Mulesoft est un ESB du monde Salesforce utilisé pour construire des flots permettant aux pièces logicielles d'un Système d'Informati...