Vous ne devez pas créer de route /login dans votre UrlMap : pour une application REST, la méthode Login est appelée implicitement lorsqu’une authentification est requise. Pour ajouter votre traitement, il faut redéfinir Login() dans votre classe qui étend %CSP.REST et appeler le parent avec ##super(skipheader), par exemple : [1][2]

ClassMethod Login(skipheader As %Boolean = 1) As %Status [ ProcedureBlock = 0 ]
{
    // votre journalisation applicative
    Quit ##super(skipheader)
}

Le fait que votre méthode ne soit pas appelée peut venir de la configuration de l’application web REST. Il a été indiqué qu’il faut laisser seulement l’authentification par mot de passe pour cette web application, retirer éventuellement “group by id”, vérifier que UnknownUser n’a pas %ALL, puis tester l’accès ; dans cette configuration, la méthode Login doit être exécutée. Dans votre cas, l’usage du JWT est explicitement mentionné comme différence de configuration. [1][2]

OnPreDispatch n’est pas adapté pour /login, /refresh et /logout : il fonctionne pour vos routes personnalisées, mais pas pour ces routes spéciales. [1][2]

Si vous avez besoin d’un point d’extension fiable autour du login, la piste proposée est d’utiliser une sous-classe de %CSP.SessionEvents, en particulier OnLogin, ou éventuellement OnStartRequest / OnEndRequest selon le besoin de journalisation. [1][2][3]


Sources:

D’après les éléments retrouvés, il n’y a pas d’indication qu’il soit possible de surcharger les méthodes internes standard de connexion comme UsersCSPLogin() ni d’intercepter la requête avant que ce traitement standard ne s’exécute dans le scénario que vous décrivez. Le contenu retrouvé montre au contraire que, lors d’un échec de connexion CSP, la pile passe bien par UsersCSPLogin dans %SYS.SECURITY, puis par CSPLogin dans %SYS.cspServer, ce qui correspond à la validation standard côté serveur que vous observez. [1][2]

Le fait que OnPreHTTP() ne soit jamais appelé pendant ce processus est cohérent avec ce qui est décrit : OnPreHTTP() est un point d’entrée d’une page CSP, invoqué avant l’envoi des en-têtes HTTP pour cette page, et il est typiquement utilisé pour une authentification privée dans une page CSP classique. Mais dans votre cas, le traitement de connexion standard semble se produire avant d’atteindre ce point pour votre page personnalisée. [1][3]

La seule piste concrète proposée dans les documents retrouvés pour obtenir un comportement de « réactivation après 15 minutes » consiste à ne pas essayer de modifier le flux interne de login standard, mais plutôt à mettre en place une tâche personnalisée exécutée périodiquement, par exemple toutes les 15 minutes, afin de réactiver les utilisateurs désactivés. Il est aussi précisé que cette approche serait plus universelle, car il existe d’autres modes de connexion que CSP. [1]

Une autre suggestion mentionnée est de ne pas utiliser l’option Disable account if login limit reached, et éventuellement de jouer uniquement sur Invalid login limit si cela répond au besoin fonctionnel. [1]

Enfin, un autre document rappelle que pour une page de login CSP personnalisée, l’utilisateur du Web Gateway CSPSystem doit avoir les droits de lecture sur la base où se trouve la classe de login personnalisée ; sinon la page personnalisée peut ne pas être chargée correctement et %CSP.Login standard continue d’être utilisée. [4]

En résumé, les documents retrouvés ne donnent pas de mécanisme pour surcharger UsersCSPLogin() ou injecter votre logique juste avant son exécution ; la solution suggérée dans ces sources est plutôt de gérer la réactivation des comptes par une tâche planifiée externe au flux de connexion standard. [1][4][2]


Sources:

L’erreur iris.cls: error finding class indique généralement que le framework IoP n’est pas chargé dans IRIS. Il faut d’abord installer le package Python, puis l’initialiser dans IRIS : pip install iris-pex-embedded-python, puis iop --init ou, en Python, from iop import Utils suivi de Utils.setup(). [1][2][3]

Il faut aussi vérifier que vous êtes dans le bon namespace IRIS via la variable d’environnement IRISNAMESPACE. [1][2]

Pour la migration, la commande documentée est iop --migrate /chemin/absolu/settings.py ou iop -M /tmp/settings.py. Le fichier settings.py doit être dans le même dossier que le code Python, et le chemin doit être absolu. [4][2][5][3]

Votre appel subprocess.run(["iop", "-m", "/external/src/CoreModel/Python/settings.py"], ...) ne correspond pas à la syntaxe montrée dans la documentation récupérée, où -m/-M est l’option de migration et attend le chemin absolu du fichier settings.py. [3]

Le fichier settings.py doit contenir au minimum :

  • CLASSES : le mapping des composants Python à enregistrer ;
  • PRODUCTIONS : la définition de la production et des items associés. [4][2][6]

Exemple minimal de structure :

from hello_world.bo import MyBo

CLASSES = {
    "MyIRIS.MyBo": MyBo
}

PRODUCTIONS = [
    {
        'MyIRIS.Production': {
            "@TestingEnabled": "true",
            "Item": [
                {
                    "@Name": "Instance.Of.MyBo",
                    "@ClassName": "MyIRIS.MyBo",
                }
            ]
        }
    }
]

[2][6]

Pour enregistrer des composants, la documentation indique aussi ces méthodes alternatives :

  • Utils.register_component(...)
  • Utils.register_file(...)
  • Utils.register_folder(...)
  • Utils.migrate() pour migrer le fichier settings.py. [4]

En pratique, l’enchaînement à vérifier est :

  1. installer iris-pex-embedded-python ;
  2. initialiser IoP dans IRIS avec iop --init ou Utils.setup() ;
  3. vérifier IRISNAMESPACE ;
  4. utiliser un settings.py valide avec CLASSES et PRODUCTIONS ;
  5. lancer la migration avec un chemin absolu, par exemple iop --migrate /external/src/CoreModel/Python/settings.py. [1][4][2][3]


Sources:

Oui : une approche proposée consiste à rendre la comparaison récursive pour traiter aussi les propriétés objets et les collections. Le principe est : vérifier d’abord que les deux objets sont de la même classe, parcourir les propriétés via %Library.ClassDefinition, puis :

  • si la propriété est un objet, rappeler la méthode de comparaison ;
  • si l’objet étend %Collection.Super, parcourir ses éléments avec Count() et GetAt(ii) puis comparer chaque élément ;
  • sinon, pour les valeurs simples, enregistrer les différences quand les valeurs sont distinctes. [1]

Exemple fourni : [1]

ClassMethod CompareObjects(expectedObject As %RegisteredObject, actualObject As %RegisteredObject) As %Status
{
    Set status= $$$OK
    Set returnValue = 1
    Try{
        // check if same class
        Set className = $CLASSNAME(expectedObject)
        If (className '= $CLASSNAME(actualObject)) {
            Set returnValue = 0
            Set ^||differences(expectedObject,actualObject) = "not same class"
        }
        If (returnValue) {
            // Get the definition to browse properties
            Set classDef = ##class(%Library.ClassDefinition).%OpenId(className)
            Set propertiesDef = classDef.Properties
            // Check the value for each property
            For i = 1:1:propertiesDef.Count() {
                Set propertyName = propertiesDef.GetAt(i).Name
                Set propertyValueExpected = $PROPERTY(expectedObject,propertyName)
                Set propertyValueActual=$PROPERTY(actualObject,propertyName)
                // if different, supply the variable difference with the name of property + value
                If $ISOBJECT(propertyValueExpected){
                    If propertyValueExpected.%Extends("%Collection.Super"){
                        For ii=1:1:propertyValueExpected.Count(){
                            Set tSC= ..CompareObjects(propertyValueExpected.GetAt(ii),propertyValueActual.GetAt(ii))
                        }
                    }
                    Else{
                        Set tSC= ..CompareObjects(propertyValueExpected,propertyValueActual)
                    }
                }
                Else{
                    If ((propertyValueExpected '= "") && (propertyValueExpected '= propertyValueActual)) {
                        Set ^||differences(expectedObject,propertyName)=propertyValueExpected _ " != " _ propertyValueActual
                    }
                }
            }
        }
    }Catch e{
        Set status=e.AsStatus()
    }
    Quit status
}

Pour restituer les écarts, l’exemple associé appelle ensuite CompareObjects, teste l’existence de ^||differences, puis construit un message en parcourant ce global temporaire. [1]

Pour les listes ObjectScript, il existe aussi un exemple séparé qui calcule les éléments ajoutés et supprimés entre deux %List en utilisant $LISTVALID, $LISTLENGTH, $LISTFIND et en retournant added / deleted. Cela peut être utile si certaines de vos propriétés sont des listes sérialisées plutôt que des objets de collection. [2]

Le même fil souligne aussi qu’avant de comparer des collections, il faut définir la règle métier : faut-il tenir compte de l’ordre ou non ? Deux collections contenant les mêmes valeurs dans un ordre différent peuvent être considérées soit identiques, soit différentes selon le cas d’usage. [2]

Si vous voulez fiabiliser encore votre méthode de parcours des propriétés, une autre source montre plusieurs façons d’énumérer les propriétés d’une classe, y compris héritées, notamment via %Dictionary.CompiledProperty ou %Dictionary.CompiledClass. [3]

En résumé, la solution la plus proche de votre besoin est de transformer votre méthode en comparaison récursive objet/collection, comme dans l’exemple ci-dessus. Pour les listes simples, vous pouvez compléter avec une logique spécialisée de diff de %List. [1][2][3]


Sources:

Le problème semble venir de l’utilisation de SendFormDataArray() avec une signature d’appel incorrecte. Dans la documentation, SendFormDataArray est défini ainsi :

Method SendFormDataArray(Output pHttpResponse As %Net.HttpResponse, pOp As %String, pHttpRequestIn As %Net.HttpRequest, pFormVarNames As %String = "", ByRef pData) As %Status [1]

Dans un autre exemple, il est aussi rappelé que votre appel doit respecter cette signature, et qu’un appel comme :

set tSC = ..Adapter.SendFormDataArray(.tHTTPResponse, "POST", tHTTPRequest, tURL, tPayload)

ne correspond pas à la signature attendue, car les arguments passés ne sont pas dans le bon ordre ni dans les bons paramètres. [2]

Dans votre code, vous faites :

set tSC = ..Adapter.SendFormDataArray(.response, "POST",httpRequest,,,..Adapter.URL_"/security/authentication")

or, d’après la signature documentée, SendFormDataArray() envoie la requête vers la destination configurée de l’adaptateur ; la variante documentée ici ne prend pas un paramètre pURL dans la signature affichée. [1] Cela suggère que passer l’URL complète ou concaténée en dernier argument n’est pas l’usage attendu pour cette méthode dans ce contexte. [1][3]

Si vous voulez simplement envoyer un corps de requête JSON, la documentation indique que les méthodes principales comme Post() peuvent être utilisées soit avec des variables de formulaire, soit avec un corps de requête. Pour envoyer un corps de requête au lieu de variables de formulaire, il faut laisser pFormVarNames vide et passer le corps en argument pData. Une valeur de corps peut être de type chaîne ou stream. [1] L’exemple fourni dans la documentation pour une opération HTTP sortante montre d’ailleurs l’utilisation de ..Adapter.Post(.tResponse,,input) pour envoyer un corps de requête. [1]

En pratique, la piste la plus solide est donc :

  1. ne pas passer l’URL concaténée dans cet appel SendFormDataArray() tel qu’il est écrit dans votre code ; [1]
  2. utiliser la destination configurée sur l’adaptateur (Server, Port, URL) ; [3]
  3. pour un POST JSON avec body, utiliser Post() plutôt que SendFormDataArray() si vous n’avez pas besoin d’un cas « inhabituel » de personnalisation. SendFormDataArray() est présenté comme une méthode bas niveau pour des cas particuliers. [1][3]

La documentation précise aussi que SendFormDataArray() est la méthode bas niveau à utiliser lorsque vous devez personnaliser la requête HTTP au-delà des simples variables de formulaire ou cookies, et que dans ce cas vous pouvez préparer un %Net.HttpRequest vous-même. [1][3] Mais rien dans les éléments retrouvés ne valide le passage de l’URL finale comme vous le faites ici. [1]

Donc, l’idée la plus probable est que l’erreur est déclenchée parce que l’URL est mal fournie à SendFormDataArray() dans votre appel, et que cette méthode n’est probablement pas la bonne pour poster votre JSON dans ce scénario. [2][1][3]

Vous pouvez vous orienter vers un appel de type :

Set tSC = ..Adapter.Post(.response,,entityBody)

en laissant l’URL dans la configuration de l’adaptateur, puisque Post() sait envoyer un request body lorsque pFormVarNames est vide. [1]

Si l’adresse doit varier dynamiquement, un retour d’expérience retrouvé indique qu’il a fallu positionner la destination sur l’adaptateur avant l’appel, par exemple :

set ..Adapter.HTTPServer = pRequest.Url
set URL = pRequest.Url_"/search"
set tSC = ..Adapter.SendFormDataArray(.tHttpResponse,"POST",tHttpRequest,,,URL)

car le problème venait justement d’une destination variable non configurée dans le composant. [4]


Sources:

D’après les éléments retrouvés, en version 2022.1, vous ne disposez pas de la méthode SetSensorLabels() ; dans l’échange correspondant à votre cas, la conclusion donnée est que cela devient possible en 2024, et que la solution retenue a été de mettre à niveau la version d’IRIS. [1]

Pour les métriques custom exposées sur /metrics, la documentation indique que :

  • on crée une classe héritant de %SYS.Monitor.SAM.Abstract ;
  • on définit les métriques dans GetSensors() avec SetSensor() ;
  • on peut ajouter des labels via SetSensorLabels() ;
  • si un label est défini pour une métrique, il faut ensuite toujours le fournir pour cette métrique. [2][3][4][5]

Note: For best practices when choosing metric and label names, see Metric and Label Naming in the Prometheus documentation (https://prometheus.io/docs/practices/naming/). [2]

IMPORTANT: A slow implementation of GetSensors() can negatively impact system performance. Be sure to test that your implementation of GetSensors() is efficient, and avoid implementations that could time out or hang. [2]

Dans votre contexte précis, la réponse la plus directe est donc : non, pas avec les API disponibles en 2022.1 si vous n’avez pas SetSensorLabels() ; pour ajouter plusieurs labels comme production, il faut passer à une version où cette méthode est disponible, comme cela a été confirmé dans le fil retrouvé. [1]

Si vous souhaitez, je peux aussi reformuler cela en réponse courte “forum”, prête à poster.


Sources:

Le comportement décrit correspond à une production active dont l’état cible ne correspond plus à son état courant. Dans ce cas, Ens.Director détecte un écart de configuration et le portail affiche le bouton rouge Update / Mettre à jour. Tant que cette mise à jour n’aboutit pas, certains hôtes peuvent rester “out-of-date”, ce qui peut empêcher l’exécution normale au moment prévu. [1]

Concrètement, une mise à jour de production compare pour chaque Business Host :

  1. les paramètres actifs,
  2. les paramètres définis dans la production / les System Default Settings / la classe,

    puis marque le host comme nécessitant une mise à jour s’il y a une différence. Ensuite, la production essaie de :
  • stopper les hosts à arrêter,
  • démarrer ceux à lancer,
  • appliquer les paramètres de production. [1]

Une cause fréquente, quand le bouton reste actif ou réapparaît, est qu’un composant ne parvient pas à s’arrêter dans le délai imparti pendant l’update. Si un host traite encore un message actif et n’a pas terminé avant le timeout, l’update échoue pour ce host, et vous voyez de nouveau le bouton rouge Update. [1]

Pour corriger cela, les pistes documentées sont :

  • augmenter le timeout d’update, si vos traitements prennent plus que la valeur actuelle ; le timeout par défaut du portail est de 10 secondes ;
  • modifier le paramètre de production Update Timeout pour une valeur plus grande ;
  • lancer explicitement ##class(Ens.Director).UpdateProduction(timeout,force) avec un délai adapté. [1]

Si votre problème apparaît au lancement de tâches planifiées, cela peut indiquer qu’au moment où elles démarrent, certains Business Hosts sont encore dans un état nécessitant une synchronisation de configuration, ou qu’un précédent update n’a pas pu se terminer correctement à cause d’un arrêt trop long d’un host. [1]

Si vous activez/désactivez des composants par code, il faut aussi éviter de demander à un composant de mettre à jour la production depuis lui-même avec EnableConfigItem(..., ..., 1). Un cas documenté montre que cela peut provoquer une erreur ErrJobRegistryNotClean, parce que le processus qui tente de mettre à jour la production essaie de se mettre à jour lui-même. La méthode recommandée est :

  • faire EnableConfigItem(..., ..., 0) sans update immédiat,
  • puis lancer UpdateProduction dans un JOB séparé. [2]

Exemple documenté : [2]

Set tSC = ##class(Ens.Director).EnableConfigItem(pConfigItemName,0,0)
Set tSC = ##class(Ens.Director).EnableConfigItem(pConfigItemName,1,0)
job ##class(Ens.Director).UpdateProduction()

Vous pouvez donc vérifier en priorité :

  • si des composants sont encore marqués out-of-date au moment du démarrage planifié ; [1]
  • si l’Update Timeout est trop court pour vos traitements ; [1]
  • si un code applicatif active/désactive des composants avec pDoUpdate=1 depuis un composant de la production lui-même. [2]

Si vous voulez, le point le plus utile à regarder ensuite est le journal d’événements de production pour identifier quel Business Host ne termine pas son arrêt pendant l’update, puisque c’est précisément le cas documenté qui laisse le bouton Mettre à jour actif. [1]


Sources: