dimanche 24 mars 2024

Transformer un contenu YAML en JAVA properties et vice versa / Transforming YAML content into JAVA properties and vice versa

Dans Mulesoft, deux formats sont privilégiés pour définir des propriétés: YAML et JAVA properties. L'amusant (enfin, pour ceux que le challenge tente...), c'est que selon l'outil, c'est l'un ou l'autre format qui est utilisé. Par exemple, sur Anypoint Studio, YAML est privilégié. Sur la console d'administration CloudHub, c'est Java Properties. Question: est-il possible de passer facilement de l'un à l'autre, par un petit script DataWeave ? Un exemple bien utile serait de pouvoir transformer la configuration en YAML d'une application en en ensemble de propriétés JAVA que l'on viendrait copier/coller dans les paramètres de l'onglet "settings" d'une application sur CloudHub.

On parle de passer de çà:

sftp:
host: "localhost"
port: "8081"

salesforce:
"token": "fghhgghjhgffddgghh"
"credentials":
"user": "Tintin"
"password": "VGFDZE"

à çà:

sftp.host = localhost
sftp.port = 8081
salesforce.token = fghhgghjhgffddgghh
salesforce.credentials.user = Tintin
salesforce.credentials.password = VGFDZE

Le script qui permet de le faire est le suivant:

%dw 2.0
output text/plain
fun yaml2json(o)=flatten (o pluck (v, k)-> (
v pluck (v2, k2)-> if ((typeOf(v2) as String)=='Object')
yaml2json((k ++ "." ++ k2):v2)
else
(k ++ "." ++ k2):v2
)
) reduce (l, a={})->(a ++ l)
fun json2Properties(o) = (o pluck {key:$$, value:$})
reduce (l, a="")-> a ++ (l["key"] ++ " = " ++ l["value"] ++ "\n")
---
json2Properties(yaml2json(payload))

Le principe consiste à prendre JSON comme format pivot, mais en réduisant la structure à un unique objets, dont les noms de chaque champ est son chemin dans la structure. Il s'agit donc d'obtenir la valeur suivante:

{
"sftp.host": "localhost",
"sftp.port": "8081",
"salesforce.token": "fghhgghjhgffddgghh",
"salesforce.credentials.user": "Tintin",
"salesforce.credentials.password": "VGFDZE"
}

Toute la magie est dans la fonction récursive yaml2json: la partie centrale (les deux pluck et le if ... else) agrège les noms des différents champs par le signe point, la partie périphérique (flatten et la reduction) aplatit la structure.

La fonction json2Properties construit une chaîne de caractères au format JAVA properties c'est à dire en concaténant toutes les lignes par un retour chariot et en associant clé et valeur à l'aide du signe égal. Pour que le résultat apparaisse proprement, il faut que le format de sortie du script soit défini comme: "plain/text".

Ce qu'on à fait dans un sens, peut-on le refaire dans l'autre ? La réponse est affirmative évidemment. Le script à utiliser est le suivant:

%dw 2.0
output application/yaml
fun field(o, v, i) = if (o[v.keys[i]] == null) (
o ++ if (i==sizeOf(v.keys)-1)
(v.keys[i]):v.value
else
(v.keys[i]):field({}, v, i+1)
) else (
o - v.keys[i] ++ (v.keys[i]):field(o[v.keys[i]], v, i+1)
)
fun prop2json(p) = p splitBy "\n"
map ($ splitBy "=")
map { keys: trim($[0]) splitBy ".", value: trim($[1]) }
reduce (v, a={}) -> field(a, v, 0)
---
prop2json(payload)

Cette fois une seule étape suffit car DataWeave reconnait le format YAML. Il suffit donc de construire la structure idoine (fonction prop2json) et de demander une sortie au format "application/yaml". La fonction procède en deux temps: elle construit une structure ou chaque association clé/valeur est structuré en autant d'objets associant un tableau des segments de clé et la valeur (c'est tout le début du contenu de la fonction) pour obtenir cela:

[
{
"keys": [
"sftp",
"host"
],
"value": "localhost"
},
{
"keys": [
"sftp",
"port"
],
"value": "8081"
},
{
"keys": [
"salesforce",
"token"
],
"value": "fghhgghjhgffddgghh"
},
{
"keys": [
"salesforce",
"credentials",
"user"
],
"value": "Tintin"
},
{
"keys": [
"salesforce",
"credentials",
"password"
],
"value": "VGFDZE"
}
]

Ce formatage est suivi par une réduction qui construit la structure arborescente en utilisant pour cela une fonction récursive "field". Le "if" extérieur de la fonction field" sépare le cas ou le segment rencontré n'est pas déjà présent dans la structure, du cas ou l'est. Le "if" intérieur sépare le cas ou la valeur rencontrée est de type primitif de celui ou c'est un objet (qui doit être être traité dans une nouvelle étape de la récursion). 
______________________________________________________________________

In Mulesoft, there are two preferred formats for defining properties: YAML and JAVA properties. The funny thing (well, for those who are up for a challenge...) is that, depending on the tool, one or the other format is used. On Anypoint Studio, for example, YAML is the preferred format. On the CloudHub administration console, Java Properties is used. Question: is it possible to switch easily from one to the other, using a small DataWeave script? A useful example would be to be able to transform an application's YAML configuration into a set of JAVA properties that you could be copy/paste'd into the settings tab of an application on CloudHub.

We're talking about starting with this:

sftp:
host: "localhost"
port: "8081"

salesforce:
"token": "fghhgghjhgffddgghh"
"credentials":
"user": "Tintin"
"password": "VGFDZE"

to obtain this:

sftp.host = localhost
sftp.port = 8081
salesforce.token = fghhgghjhgffddgghh
salesforce.credentials.user = Tintin
salesforce.credentials.password = VGFDZE

The script to do this is as follows:

%dw 2.0
output text/plain
fun yaml2json(o)=flatten (o pluck (v, k)-> (
v pluck (v2, k2)-> if ((typeOf(v2) as String)=='Object')
yaml2json((k ++ "." ++ k2):v2)
else
(k ++ "." ++ k2):v2
)
) reduce (l, a={})->(a ++ l)
fun json2Properties(o) = (o pluck {key:$$, value:$})
reduce (l, a="")-> a ++ (l["key"] ++ " = " ++ l["value"] ++ "\n")
---
json2Properties(yaml2json(payload))

The principle is to use JSON as the pivot format, but reduce the structure to a single object, with the name of each field being its path through the structure. The aim is to obtain the following value:

{
"sftp.host": "localhost",
"sftp.port": "8081",
"salesforce.token": "fghhgghjhgffddgghh",
"salesforce.credentials.user": "Tintin",
"salesforce.credentials.password": "VGFDZE"
}

The magic is in the recursive yaml2json function: the central part (the two plucks and the if ... else) aggregates the names of the various fields using the dot sign, while the peripheral part (flatten and reduction) flattens the structure.

The json2Properties function constructs a string in JAVA properties format, i.e. concatenating all lines with a carriage return and associating key and value with the equal sign. For the result to be displayed correctly, the script output format must be defined as "plain/text".

Can you do the same thing in the other direction? The answer is yes, of course. The script to use is as follows:

%dw 2.0
output application/yaml
fun field(o, v, i) = if (o[v.keys[i]] == null) (
o ++ if (i==sizeOf(v.keys)-1)
(v.keys[i]):v.value
else
(v.keys[i]):field({}, v, i+1)
) else (
o - v.keys[i] ++ (v.keys[i]):field(o[v.keys[i]], v, i+1)
)
fun prop2json(p) = p splitBy "\n"
map ($ splitBy "=")
map { keys: trim($[0]) splitBy ".", value: trim($[1]) }
reduce (v, a={}) -> field(a, v, 0)
---
prop2json(payload)

This is a one-step process, as DataWeave recognizes the YAML format. All you need to do is build the appropriate structure (prop2json function) and request output in "application/yaml" format. The function proceeds in two stages: it builds a structure where each key/value association is structured into as many objects associating an array of key segments and the value (this is the very beginning of the function's content) to obtain this:

[
{
"keys": [
"sftp",
"host"
],
"value": "localhost"
},
{
"keys": [
"sftp",
"port"
],
"value": "8081"
},
{
"keys": [
"salesforce",
"token"
],
"value": "fghhgghjhgffddgghh"
},
{
"keys": [
"salesforce",
"credentials",
"user"
],
"value": "Tintin"
},
{
"keys": [
"salesforce",
"credentials",
"password"
],
"value": "VGFDZE"
}
]

This formatting is followed by a reduction that builds the tree structure, using a recursive "field" function. The outer "if" of the field function separates the case where the segment encountered is not already present in the structure, from the case where it is. The inner "if" separates the case where the encountered value is of primitive type from the case where it is an object (which must be processed in a new step of the recursion). 



vendredi 8 mars 2024

CONSIGNES: Defaulting

 Cet article regroupe les consignes concernant les valeurs à utiliser par défaut et que doivent suivre les développeurs de VISEO qui travaillent avec moi. Il m'a paru intéressant de les regrouper ici en un article afin qu'elles soient très largement disponibles.

Le "defaulting" de Mulesoft est en général très bon. Il y a néanmoins quelques trous dans la raquette et si vous suivez les consignes données ci-dessous vous vous éviterez bien des ennuis. Cet article a vocation à être régulièrement enrichi, au fur et à mesure de nos "découvertes". Commençons:


Reconnexion

C'est le defaulting de Mulesoft le plus étrange, provoquant, selon mon expérience 50% des bugs rencontrés en production. Par défaut, l'option de reconnexion est fixée par Anypoint Studio à "None", c'est à dire que si la connexion tombe (qu'elle soit HTTP, VM, Object Store, SFTP, etc, etc), elle ne pourra pas être rétablie. Un redémarrage du serveur est alors inévitable. Non seulement ce defaulting est aberrant, mais le fait même de simplement proposer cette option, nécessiterait une explication que je n'ai pas ! Cerise sur le gâteau, il existe une option "Standard" qui comme son nom l'indique ferait une excellente valeur par défaut! Bref, n'oubliez JAMAIS de changer cette option sur tous les objets de configuration (volet "Advanced"):


La valeur des autres paramètres est plus dépendante de votre contexte. Mais les valeurs fournies par Anypoint Studio sont généralement adaptées.
Si vous tentez de vous connecter avec un outil/ressource dont le fonctionnement peut présenter de longues périodes de panne, optez alors pour l'option "forever" en augmentant la valeur du paramètre fréquence à quelque chose de l'ordre de la minute afin d'éviter de mitrailler les systèmes, de requêtes:


J'insiste lourdement sur ce défaulting car il a tout du piège dangereux: en développement, voir en tests utilisateurs, tout semblera bien se passer, car le système ne restera probablement pas actif suffisamment longtemps pour que le connexion tombe. En production, en revanche, cela arrivera fatalement.

Si quelqu'un a une idée sur l'intérêt que peut avoir l'option "None", qu'il prenne contact avec moi. Je l'en remercie d'avance.

Replica Count

Mulesoft insiste, aussi bien dans la documentation, que dans les formations et les forums, sur le fait qu'il faut monter au moins 2 réplicas pour chaque application, afin de garantir une meilleure continuité de service. Je souscris pleinement à ce conseil. Mais pourtant, lorsque l'on déclare une instance, c'est systématiquement 1 réplica qui est proposé par défaut. Donc n'oubliez pas, même dans les environnements qui ne sont pas en production:
  1. de demander au moins 2 réplicas (c'est généralement suffisant)
  2. de cocher l'option "Run In Runtime Cluster Mode" (bas de la page) qui est décoché par défaut.





Sur CloudHub 1.0, c'est déjà une sage précaution. Sur CloudHub 2.0, c'est carrément une nécessité: car la persistance de VM et d'Object Store en dépend ! En effet, tout le contenu de ces deux mécanismes est perdu dès qu'il n'a plus d'instance active. Moralité, si vous redéployez, vous perdez tout ! Que de drames, de faux bugs et de suspicions ai-je rencontré sur les projets parce que les caches ou des données dynamiques de configuration "disparaissent" quand un développeur uploade une nouvelle version.  Mulesoft serait bien inspiré de modifier ce defaulting rapidement.

Le mode Cluster est indispensable avec CloudHub 2.0, non seulement pour assurer la persistence de VM et d'Object Store, mais aussi pour éviter que Mulesoft ne traite "en double" vos fichiers/SFTP et autres scheduler !

Si quelqu'un sait pourquoi Object Store et VM ne sont plus vraiment persistents, sachez que je suis demandeur d'une explication. Cette raison seule me fait préférer CloudHub 1.0 à 2.0 !

Primary Mode Only

Encore un defaulting Mulesoft que j'ai bien du mal à m'expliquer. Toutes Sources de type "On Event" (On new or updated file, On New Message, On New Email) proposent une option "Primary Mode Only" qui n'est jamais cochée par défaut. Noter que les Listeners proposent la même option. Dans le cas des Listeners, le defaulting de cette case à cocher est le bon, même si à l'occasion, il peut être utile ou nécessaire de la changer. 



Dans le cas des composants "On Event", il ne convient pas de systématiquement cocher cette option. Il ne faut le faire de manière systématique que dans le cas ou la simple consommation de la ressource ne provoque pas sa non disponibilité immédiate (comme sa disparition par exemple). C'est le cas en particulier des Sources de type "On New Or Updated File" des connecteurs File, FTP ou SFTP.

En effet, dans le cas de ces Sources, si deux instances Mulesoft sont susceptibles de traiter l'évènement, elles peuvent se faire concurrence. Ce qui revient à traiter l'évènement deux fois. Or dans la quasi totalité des cas, un double traitement entraine des dysfonctionnements. Il faut donc réserver cette fonction à une seule instance de serveur et donc cocher l'option "Primary Node Only".

_____________________________________________________________________

This article brings together the default values that VISEO developers working with me must follow. I thought it would be a good idea to group them together here in one article, so that they are widely available.
Mulesoft's defaulting is generally very good. There are a few holes in the system, however, and if you follow the instructions given below, you'll save yourself a lot of trouble. This article is intended to be regularly enriched, as and when we make "discoveries". Let's get started:

Reconnect

This is Mulesoft's strangest defaulting, causing, in my experience, 50% of the bugs encountered in production. By default, the reconnection option is set by Anypoint Studio to "None", i.e. if the connection drops (whether HTTP, VM, Object Store, SFTP, etc, etc), it cannot be re-established. A server restart is then inevitable. Not only is this defaulting aberrant, but the very fact of proposing this option would require an explanation that I don't have! The icing on the cake is that there is a "Standard" option which, as its name suggests, would make an excellent default value! In short, NEVER forget to change this option on all configuration objects ("Advanced" pane):



The values of the other parameters are more dependent on your context. However, the values provided by Anypoint Studio are generally appropriate.
If you're trying to connect with a tool/resource that may be subject to long periods of downtime, then you should opt for the "forever" option, increasing the value of the frequency parameter to something on the order of a minute, to avoid peppering systems with requests:


I'm insisting heavily on this defaulting because it's a dangerous trap: in development, or even in user testing, everything will seem to be going well, as the system will probably not remain active long enough for the connection to fall out. In production, on the other hand, this is bound to happen.

If anyone has any ideas about the usefulness of the "None" option, please get in touch with me. Thank you in advance.

Replica Count

In its documentation, training courses and forums, Mulesoft insists on the need to set up at least 2 replicas for each application, to guarantee better continuity of service. I fully subscribe to this advice. However, when an instance is declared, 1 replica is systematically proposed by default. So don't forget, even in non-production environments:
  1. request at least 2 replicas (this is generally sufficient)
  2. check the "Run In Runtime Cluster Mode" option (bottom of page), which is unchecked by default.





On CloudHub 1.0, this is already a wise precaution.On CloudHub 2.0, it's an absolute necessity: because VM and Object Store persistence depend on it!Indeed, all the content of these two mechanisms is lost as soon as there is no longer an active instance.If you redeploy, you lose everything! What drama, false bugs and suspicions I've encountered on projects because caches or dynamic configuration data "disappear" when a developer uploade a new version.  Mulesoft would be well advised to change this defaulting quickly.

Cluster mode is essential with CloudHub 2.0, not only to ensure VM and Object Store persistence, but also to prevent Mulesoft from "duplicating" your files/SFTP and other schedulers!

If anyone knows why Object Store and VM aren't really persistent anymore, I'd love to hear about it. This reason alone makes me prefer CloudHub 1.0 to 2.0!

Primary Mode Only

Another Mulesoft defaulting that I'm having trouble explaining to myself. All "On Event" Sources (On new or updated file, On New Message, On New Email) offer a "Primary Mode Only" option which is never checked by default.Note that Listeners offer the same option.In the case of Listener, the default setting of this checkbox is the correct one, although it may occasionally be useful or necessary to change it.



In the case of "On Event" components, this option should not be systematically checked. It should only be systematically ticked if the simple consumption of the resource does not result in its immediate non-availability (e.g. its disappearance).This is particularly the case for "On New Or Updated File" sources on File, FTP or SFTP connectors.

Indeed, in the case of these Sources, if two Mulesoft instances are likely to process the event, they can compete with each other. This means processing the event twice. In almost all cases, double processing leads to malfunctions. You should therefore reserve this function for a single server instance, and check the "Primary Node Only" option.



mercredi 6 mars 2024

Conserver une requête / Keep a query

Un problème assez commun que l'on doit traiter avec Mulesoft est la conservation de requêtes. De plus en plus souvent, nous connectons nos serveurs Mulesoft à des services qui délivrent des informations au fil de l'eau, au travers de ce qui est trop vite nommé des "Webhooks". Nous reparlerons prochainement des avantages et limites de telles architectures. Contentons nous aujourd'hui de répondre à une question: si je ne peut traiter dans l'immédiat une sollicitation pour une raison ou une autre (indisponibilité d'un service par exemple), comment puis-je m'assurer que je ne vais pas la perdre et pourrai la resoumettre plus tard ?

En fait, avec Mulesoft, c'est assez facile à implémenter en utilisant deux mécanismes:

  1. les queues de messages,
  2. les transactions (XA si nécessaires)

L'idée est simple : lorsque la requête est reçue,  elle est stockée dans une queue de messages. Un autre flux, sous transaction celui-là viendra consommer la requête transformée en message:

Voici la configuration du listener VM (pour la partie transaction):

Dans cet exemple, comme il n'est pas nécessaire d'effectuer de mises à jour distribuées, une transaction locale est suffisante. Mais votre cas peut nécessiter d'utiliser un mode de transaction plus puissant et passer en XA (2 phase commit).

Si le fait d'utiliser une queue de message pour conserver les requêtes est parfois identifié comme une solution de "replay" par les équipes que je côtoie, le mécanisme est plutôt vu comme un mécanisme de gestion d'erreur. Le principe adopté est:

  1. quand je reçois la requête, de façon synchrone, je tente de la traiter.
  2. si je rencontre un problème (partie gestion d'erreur) alors j'expédie la requête dans une queue de messages pour traitement ultérieur
  3. je mets en place un flux capable d'exploiter le contenu de cette queue de messages. Ce flux, globalement, suit le même principe qu'en 1.

C'est faire la même chose en plus compliqué (et donc en plus fragile). S'il n'y a pas de contre-indication pour passer systématiquement par un traitement asynchrone (ce qui est la plupart du temps le cas), simplifions : une requête entrante est systématiquement conservée dans la queue de message. On m'avance parfois un potentiel gain de performance : il est toujours pratiquement nul. Résumons: faites simple.

Second point: quelle système de queue de messages utiliser ? Réponse : quelque chose qui soit performant et fiable. C'est à dire, entre autres, qui puisse s'inscrire dans une transaction. Personnellement, j'utilise la plupart du temps VM. Dans le cas de CloubHub 1.0, ou onPremise, c'est parfaitement justifié. Pour CloudHub 2.0, avec l'abandon d'une "vraie" persistance pour VM et Object Store, c'est plus discutable. L'adoption de Anypoint MQ ne me parait pas pertinent : on ne peut l'inclure dans une transaction ce qui complexifie son utilisation dans notre cas. Reste l'utilisation d'un produit du marché, répondant à la norme Java JMS (comme RabbitMQ, Tibco, ...) : il faut acheter, installer, configurer. Bref, je dirai, par ordre de pertinence:

  1. Si on utilise CloudHub 1.0 ou OnPremise "Bare Metal" : VM
  2. Si on utilise CloudHub 2.0 (ou RuntimeFabric) et que la perte de messages n'est pas une catastrophe irréparable: VM en faisant bien attention à déclarer au minimum 2 instances (ce qui est une très saine précaution, bien au delà de l'utilisation de VM)
  3. Si le perte d'un message est potentiellement une catastrophe absolue: JSM.
  4. Si on aime les challenges techniques (et qu'on dispose par ailleurs du produit): Anypoint MQ avec l'implémentation d'un solide pattern de recouvrement (hors du propos de cet article).
Le dernier sujet est la prise en compte de l'impossibilité de traiter la requête, c'est à dire qu'en dépit de tentatives répétées, on ne peut réussir. Cela peut être du:
  1. au fait qu'une application ou une ressource reste indisponible (j'ai vu des instances SAP être off pendant des jours...)
  2. au fait que le message entrant est erroné et n'est pas traitable en l'état.

Dans les deux cas,  la solution consiste à rediriger le message vers une queue d'erreur généralement appelée Dead-End-Queue (DEQ) ou tout type de conservation équivalent.

VM permet de faire çà très facilement: il suffit d'indiquer le nombre maximum de tentatives que l'on souhaite. L'exception émise est MULE:REDELIVERY_EXHAUSTED :


Pousser un message vers un DEQ ou un système persistent est une chose. Etre en capacité d'exploiter cette DEQ en est une autre. Je vois trop souvent, des équipes implémenter une DEQ sans se préoccuper de la manière dont on l'utilisera. Cela devient effectivement une impasse ! C'est un vrai sujet en soi. Nous en reparlerons dans un prochain article.

____________________________________________________________________

A fairly common problem we have to deal with with Mulesoft is request retention. More and more often, we're connecting our Mulesoft servers to services that deliver information on the fly, through what are all too quickly dubbed "Webhooks". We'll come back to the advantages and limitations of such architectures shortly. For now, let's just answer one question: if I can't immediately process a request for one reason or another (unavailability of a service, for example), how can I be sure I won't lose it and be able to resubmit it later?

In fact, with Mulesoft, this is quite easy to implement, using two mechanisms:

  1. message queues,
  2. transactions (XA if necessary)

The idea is simple: when a request is received, it is stored in a message queue. Another flow, this one under a transaction, will consume the request transformed into a message:

Here's the VM listener configuration (for the transaction part):

In this example, as no distributed updates are required, a local transaction is sufficient. But your case may require you to use a more powerful transaction mode and switch to XA (2 phase commit).

While using a message queue to hold requests is sometimes identified as a "replay" solution by the teams I work with, the mechanism is seen more as an error management mechanism. The principle adopted is:

  1. when I receive the request, synchronously, I try to process it.
  2. if I encounter a problem (error handling part), I send the request to a message queue for further processing
  3. I set up a flow capable of exploiting the contents of this message queue. Overall, this flow follows the same principle as in 1.

It's doing the same thing, only more complicated (and therefore more fragile). If there's no reason to systematically use asynchronous processing (which is most of the time the case), let's simplify: an incoming request is systematically stored in the message queue. I'm sometimes told of a potential performance gain: it's always practically nil. To sum up: keep it simple.

Second point: which message queuing system to use? Answer: something that's efficient and reliable. That is, among other things, something that can be part of a transaction. Personally, I use VM most of the time. In the case of CloubHub 1.0, or onPremise, this is perfectly justified. For CloudHub 2.0, with the abandonment of "true" persistence for VM and Object Store, it's more questionable. Adopting Anypoint MQ doesn't seem relevant to me: you can't include it in a transaction, which complicates its use in our case. That leaves the use of an off-the-shelf product that complies with the Java JMS standard (such as RabbitMQ, Tibco, etc.): you have to buy, install and configure it. In short, in order of relevance:

  1. If using CloudHub 1.0 or OnPremise "Bare Metal": VM
  2. If using CloudHub 2.0 (or RuntimeFabric) and message loss is not an irreparable catastrophe: VM, taking care to declare at least 2 instances (which is a very healthy precaution, well beyond the use of VMs).
  3. If the loss of a message is potentially an absolute catastrophe: JSM.
  4. If you like technical challenges (and have the product at your disposal): Anypoint MQ with the implementation of a solid recovery pattern (outside the scope of this article).
      The last topic is the consideration of the impossibility of processing the query, i.e. that despite repeated attempts, we can't succeed. This may be due to:
      1. the fact that an application or resource remains unavailable (I've seen SAP instances be off for days...)
      2. the incoming message is erroneous and cannot be processed as it stands.
      In both cases, the solution is to redirect the message to an error queue, generally called Dead-End-Queue (DEQ), or any equivalent type of conservation.

      VM makes this very easy: all you have to do is specify the maximum number of retries you want. The exception is MULE:REDELIVERY_EXHAUSTED :

      Pushing a message to a persistent DEQ or system is one thing. Being able to exploit that DEQ is quite another. All too often, I see teams implementing a DEQ without thinking about how it will be used. This becomes a real dead end! It's a real subject in itself. We'll talk more about it in a future article.

      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.




        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...